Console

·

8 min read

Building a Guest Book on the Edge with SvelteKit, Upstash Redis and Vercel

Geoff Rich

Guest Author

Starter overview
Getting the user’s location
Saving the user’s location to Redis
Deploying to Vercel
Wrapping up

Vercel recently launched Edge Functions, which let you run JavaScript code on their globally-distributed edge network. With SvelteKit, the upcoming full-stack framework for Svelte, you can deploy your app to these Edge Functions. This means you can serve your users dynamically-rendered pages at the same speed you would serve static files from a CDN.

In this tutorial, we’ll show how to build a small site with SvelteKit that takes full advantage of the edge. The site will show a list of all the cities that have visited the site. We’ll determine the user’s current city using Vercel’s edge headers and display it on the page, and the user can choose to add their city to the list of visitors.

For a preview of what we’re building, see the live demo.

We’ll be using Upstash Redis as a data store, since (like SvelteKit) it’s optimized for use with serverless hosting providers. In addition, they allow provisioning a global database that will minimize latency from the deployed function to the database instance.

A word of caution before starting this tutorial: at time of writing, both SvelteKit and Vercel Edge Functions are currently in beta and the APIs might change.

Starter overview

Set up the starter project by running the following commands. This uses a tool called degit to download a fresh copy of the initial state of the repo, though you can clone/fork the initial branch of the starter repo itself if you’d prefer.

npx degit geoffrich/sveltekit-edge-guestbook#initial sveltekit-edge-guestbook
cd sveltekit-edge-guestbook
npm i
npm run dev

Currently, it’s non-functional since it’s not getting the user’s city or storing the list of cities anywhere. In this blog post, we’ll fix both of those issues.

Let’s quickly orient ourselves in the project. src/routes/index.svelte corresponds to the page we’ll see at the root route. It takes props for the list of visits, the city of the current user, and whether the user has signed the guest book or not.

One cool thing is that the data is submitted via a <form>, so it does not need JavaScript to function. When JS is not available, the form will submit as normal and trigger a full page reload. When JS is available, it will update in place instead of reloading. This is accomplished using a Svelte action called enhance, which is copied from the default SvelteKit project template. Explaining how it works is outside of the scope of this post, but take a look at the code in src/lib/enhance.ts if you’re curious.

<form
	action="/"
	method="post"
	use:enhance={{
		result: () => {
			signed = true;
		}
	}}
>
	<button>I was here</button>
</form>

In src/routes/index.ts, we have our main page endpoint. The exported GET function will supply the props to our index.svelte page and the POST function will handle the POST request from the form. If you’re not familiar with SvelteKit’s endpoints, see the documentation for more information on how they work.

Currently, the endpoints call the get_visitors and add_visitor functions from our $lib directory to retrieve and update the list of visits. They also call a local get_city method to get the user’s city. Those methods are stubbed out for now, since we’ll be implementing them as part of this tutorial.

Getting the user’s location

Since we’re planning to deploy the app to Vercel, we can use Vercel’s edge headers to get the user’s city and country. x-vercel-ip-country will have the country code of the user and x-vercel-ip-city will have the city of the user. This is all based off of the user’s IP address, so it won’t be 100% accurate. However, it will be good enough for our purposes.

The relevant headers names are already in src/lib/constants.ts for our use. Import them at the top of src/routes/index.ts.

import { CITY_HEADER, COUNTRY_HEADER } from '$lib/constants';

Then replace the existing get_city function with the following:

function get_city(request: Request) {
	const city = request.headers.get(CITY_HEADER) ?? 'unknown city';
	const country = get_country_name(request.headers.get(COUNTRY_HEADER));
	return `${city}, ${country}`;
}

const display_names = new Intl.DisplayNames(['en'], { type: 'region' });
function get_country_name(countryCode: string | null) {
	if (countryCode) {
		return display_names.of(countryCode);
	}
	return 'unknown country';
}

This method will retrieve the city and country from the request headers, falling back to “unknown” if it’s not set. Note how this function uses the web Intl.DisplayNames API to get the country name from the country code—no NPM package required! This API will be available when we deploy to Vercel Edge Functions.

This will work great when deployed, but these headers won’t be automatically set when developing. Fortunately, the starter project already adds custom code in SvelteKit’s handle hook to add these headers in development.

// src/hooks.ts
import type { Handle } from '@sveltejs/kit';
import { COUNTRY_HEADER, CITY_HEADER } from '$lib/constants';

export const handle: Handle = async function ({ event, resolve }) {
	if (import.meta.env.DEV) {
		event.request.headers.append(CITY_HEADER, 'Kakariko Village');
		event.request.headers.append(COUNTRY_HEADER, 'JP');
	}
	const response = await resolve(event);
	return response;
};

With these changes, you can deploy the project and the page will display the city the request came from. However, we still need to actually store the city when the user submits the form. Let’s do that next.

Saving the user’s location to Redis

When the user submits the form, we want to add their city to a list. This list should store both the city name as well as the number of times this city has been submitted. This is perfect for a Redis sorted set, since each member in the set has a value (the city name) and score (the number of times that city has visited).

For the purposes of this tutorial, we’ll use Upstash and their JavaScript SDK to make interacting with Redis as straightforward as possible.

Go ahead and sign up for a free Upstash instance to continue following along with this tutorial. A single region should be fine for development, though when you deploy production applications to the edge you may want to use a multi-region database to reduce latency between the edge function processing the request and the database.

Once you’ve created a Redis instance, create a .env file at the root of the repo with your REST URL and token. You can find these values in the “REST API” section of your database settings in the Upstash console.

UPSTASH_REDIS_REST_URL="COPY URL HERE"
UPSTASH_REDIS_REST_TOKEN="COPY TOKEN HERE"

The project is set up to use env-cmd in development, which will make these values available to your application. In production, we will set these values in the Vercel console.

Now, update our get_visitors and add_visitor functions in /src/lib/data.ts to read and write data from our Redis instance.

import { Redis } from '@upstash/redis';
import type { Visit } from './types';

const set_name = 'visitors';

export async function get_visitors(): Promise<Visit[]> {
	const redis = Redis.fromEnv();
	const visitors = await redis.zrange<string[]>(set_name, 0, -1, { withScores: true });
	return adapt_visitors(visitors);
}

export async function add_visitor(city: string) {
	const redis = Redis.fromEnv();
	await redis.zincrby(set_name, 1, city);
}

// takes the result from ZRANGE and adapt it into a list of visits
// e.g. ['Seattle', '2', 'Los Angeles', '1'] becomes
// [ { city: 'Seattle', count: '2'}, { city: 'Los Angeles', count: '1' } ]
function adapt_visitors(visitors: string[]): Visit[] {
	const adapted: Visit[] = [];
	for (let i = 0; i < visitors.length; i += 2) {
		const member = visitors[i];
		const score = visitors[i + 1];
		adapted.push({ city: decodeURIComponent(member), count: score });
	}
	return adapted;
}

In get_visitors, we first initialize our Redis client using the built-in fromEnv function. This will grab the environment variables we set earlier. We then use the ZRANGE command to retrieve everything in the visitors set and their scores. The data returned is not in the format we want, so we convert it into a list of visit objects and return it.

add_visitor is simpler. It gets the Redis client as in the previous method, and uses ZINCRBY to add one to that city in the visitors set. By default, ZINCRBY will add the item to the set if it isn’t already there.

These methods are already used in our index.ts page endpoint, so you should now be able to add cities to the list and see the results.

Deploying to Vercel

With those changes in place, we can deploy the project to Vercel Edge Functions. In SvelteKit, this is as simple as using the Vercel adapter and passing {edge: true} to the adapter options. This should already be done in the provided starter project.

import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://github.com/sveltejs/svelte-preprocess
	// for more information about preprocessors
	preprocess: preprocess(),

	kit: {
		adapter: adapter({ edge: true })
	}
};

export default config;

Push your project to GitHub and create a new project on Vercel. Everything should be preconfigured, though you’ll want to add the Upstash environment variables to the project that we previously added to our .env file. Wait for the project to build and deploy, and your site should be live! SvelteKit, Vercel, and Upstash—running on the edge 😎

Wrapping up

Thanks for following along! I hope this post showed how easy it is to deploy a site to the edge using SvelteKit. You can find the final code on GitHub and the live demo deployed to Vercel. For another example of SvelteKit running on Edge Functions, see this project from Rich Harris.

Have any questions? Reach out on Twitter. You can also find my other writing about Svelte on my blog.



© 2022 Upstash, Inc. Based in California.

* Redis is a trademark of Redis Labs Ltd. Any rights therein are reserved to Redis Ltd. Any use by Upstash is for referential purposes only and does not indicate any sponsorship, endorsement or affiliation between Redis and Upstash.

** Cloudflare, the Cloudflare logo, and Cloudflare Workers are trademarks and/or registered trademarks of Cloudflare, Inc. in the United States and other jurisdictions.