Feature Image

SvelteKit Cheat Sheet

Jul 29, 2023

9 min read

SvelteKit is a powerful framework for building web applications that brings together the simplicity of Svelte's component-based approach with advanced features for routing, server rendering, and more. This cheat sheet captures key concepts and techniques to help developers quickly reference and apply their knowledge.


File Structure

Server Side

  • +hook.server.svelte - Server hooks
  • +layout.svelte - Share common UI using <slot>
  • +layout.server.js - Load data for every child route
  • +server.js - API only (doesn't need +page.svelte)

Client Side

  • +page.server.js - Load data and handle API requests
  • +page.js - Dynamic component loading
  • +page.svelte - Page content

Setting Cookies

js
export function load({ cookies }) {
const visited = cookies.get('visited');
cookies.set('visited', 'true', { path: '/' });
return {
visited
};
}

Props

<script>
export let data;
</script>
<h1>Hello {data.visited ? 'friend' : 'stranger'}!</h1>

Shared Modules

Location: src/lib

Use $lib to access modules instead of relative paths like ../../


Forms

Default Form

<form method="POST">
<label>
add a todo:
<input
name="description"
autocomplete="off"
/>
</label>
</form>
js
export const actions = {
default: async ({ cookies, request }) => {
const data = await request.formData();
db.createTodo(cookies.get('userid'), data.get('description'));
}
};

Named Form Actions

js
export const actions = {
create: async ({ cookies, request }) => {
const data = await request.formData();
db.createTodo(cookies.get('userid'), data.get('description'));
},
delete: async ({ cookies, request }) => {
const data = await request.formData();
db.deleteTodo(cookies.get('userid'), data.get('id'));
}
};
<form method="POST" action="?/create">

If the action was defined on another page, you might have something like /todos?/create. Since the action is on this page, we can omit the pathname altogether, hence the leading ? character.

<form method="POST" action="?/delete">

Note: Data updates automatically without writing any fetch code.


Validation and Error Handling

Server Side

js
// Check for duplicates
if (todos.find((todo) => todo.description === description)) {
throw new Error('todos must be unique');
}
import { fail } from '@sveltejs/kit';
try {
db.createTodo(cookies.get('userid'), data.get('description'));
} catch (error) {
return fail(422, {
description: data.get('description'),
error: error.message
});
}

Client Side

Access the returned value via the form prop:

<script>
export let data;
export let form;
</script>
<h1>todos</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}

Progressive Enhancement

Run JS code when submitting a form. Useful when a page has many actions within the same URL. Enhance element behaviors using JS without reloading when updating the page.

js
import { enhance } from '$app/forms';

Add the use:enhance directive to <form> elements:

<form method="POST" action="?/create" use:enhance={func}>

SubmitFunction

ts
function enhance<
Success extends Record<string, unknown> | undefined,
Failure extends Record<string, unknown> | undefined
>(
form_element: HTMLFormElement,
submit?: import('@sveltejs/kit').SubmitFunction<Success, Failure>
): {
destroy(): void;
};

Example:

<form method="POST"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
// `formElement` is this `<form>` element
// `formData` is its `FormData` object that's about to be submitted
// `action` is the URL to which the form is posted
// calling `cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted
// callback
return async ({ result, update }) => {
// `result` is an `ActionResult` object
await update({reset: true});
// the default logic that would be triggered if this callback wasn't set
};
}}
>

+server.js

Use +server.js files to expose (for example) a JSON API.

<!-- +page.svelte -->
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button on:click={rerun}>Rerun CI</button>
js
// +server.js
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}

Example

js
// src/routes/todo/[id]/+server.js
import * as database from '$lib/server/database.js';
export async function PUT({ params, request, cookies }) {
const { done } = await request.json();
const userid = cookies.get('userid');
await database.toggleTodo({ userid, id: params.id, done });
return new Response(null, { status: 204 });
}
export async function DELETE({ params, cookies }) {
const userid = cookies.get('userid');
await database.deleteTodo({ userid, id: params.id });
return new Response(null, { status: 204 });
}
// src/routes/+page.svelte
await fetch(`/todo/${todo.id}`, {
method: 'PUT',
body: JSON.stringify({ done }),
headers: {
'Content-Type': 'application/json'
}
});

Built-in Stores

Access Route Information

js
import { page, navigating, updated } from '$app/stores';
// $page.url.pathname
// $navigating.to.url.pathname

Handling Page Version Updates

{#if $updated}
<p class="toast">
A new version of the app is available
<button on:click={() => location.reload()}>
reload the page
</button>
</p>
{/if}

Error Handling

Throwing Errors

Expected errors (no log and stack trace):

js
import { error } from '@sveltejs/kit';
throw error(420, 'enhance your calm');

Unexpected errors (a bug in the app, has log and stack trace):

js
throw new Error('Kaboom!');

Error Page

Create +error.svelte to handle errors.


Redirect

js
import { redirect } from '@sveltejs/kit';
export function load() {
throw redirect(307, '/b');
}
// Status codes:
// 303 — for form actions, following a successful submission
// 307 — for temporary redirects
// 308 — for permanent redirects

Helper function for login redirects:

js
export function handleLoginRedirect(
event,
message = "You must be logged in to access this page"
) {
const redirectTo = event.url.pathname + event.url.search;
return `/login?redirectTo=${redirectTo}&message=${message}`;
}
export const load = async (event) => {
if (!event.locals.user) {
throw redirect(302, handleLoginRedirect(event));
}
};

Client-Side Navigation

Using goto

<script>
import { FilePlus } from 'lucide-svelte';
import { Button } from '$components/ui/button';
import { goto } from '$app/navigation';
function create_post() {
goto('/blog/create');
}
export let data;
</script>
<svelte:head>
<title>Blog</title>
<meta name="description" content="About this app" />
</svelte:head>
<div class="flex flex-row space-x-4">
<div class="basis-1/4 items-center justify-center flex flex-col space-y-4">
<div>
<Button class="w-max" on:click={create_post}>
<FilePlus class="mr-2 h-4 w-4" /> New
</Button>
</div>
<div>tags</div>
</div>
<div class="grow items-center justify-center">02</div>
</div>

Hooks

Middleware

Intercept and override the framework's default behavior.

Intercepting Pages

js
// hooks.server.js
export async function handle({ event, resolve }) {
// do something with event
// access the page.server.js
let response = await resolve(event);
// do something with response
return response;
}

Intercepting Fetches

Can be used to make credentialed requests on the server:

js
export async function handleFetch({ event, request, fetch }) {
const url = new URL(request.url);
if (url.pathname === '/a') {
return await fetch('/b');
}
return await fetch(request);
}
export async function load({ fetch }) {
const response = await fetch('/a'); // intercepted fetch
return {
message: await response.text()
};
}

Page Options

Configure in +page.server.js:

  • ssr — whether or not pages should be server-rendered
  • csr — whether to load the SvelteKit client
  • prerender — whether to prerender pages at build time, instead of per-request
  • trailingSlash — whether to strip, add, or ignore trailing slashes in URLs
js
export const ssr = false;
js
export const csr = false;

This means that no JavaScript is served to the client.

js
export const prerender = true;

The advantage is that serving static data is extremely cheap and performant, allowing you to easily serve large numbers of users without worrying about cache-control headers (which are easy to get wrong).

Setting prerender to true inside your root +layout.server.js effectively turns SvelteKit into a static site generator (SSG).


Preloading

<a href="/slow-a" data-sveltekit-preload-data>slow-a</a>

SvelteKit will begin the navigation as soon as the user hovers over the link (on desktop) or taps it (on mobile).

Preload strategies:

  • "eager" — preload everything on the page following a navigation
  • "viewport" — preload everything as it appears in the viewport
  • "hover" (default) — preload on hover/tap
  • "tap" — preload on tap only
  • "off" — disable preloading

Preloading Programmatically

js
import { preloadCode, preloadData } from '$app/navigation';
// preload the code and data needed to navigate to /foo
preloadData('/foo');
// preload the code needed to navigate to /bar, but not the data
preloadCode('/bar');

Reloading

SvelteKit holds page snapshots even when switching pages. To disable this behavior, add the data-sveltekit-reload attribute:

<nav data-sveltekit-reload>
<a href="/">home</a>
<a href="/about">about</a>
</nav>

Routing

Optional Route Parameters

src/routes/[[lang]]/+page.server.js
js
const greetings = {
en: 'hello!',
de: 'hallo!',
fr: 'bonjour!'
};
export function load({ params }) {
return {
greeting: greetings[params.lang ?? 'en']
};
}

Route Regular Expression Match

src/routes/colors/[color=hex]
js
// src/params/hex.js
export function match(value) {
return /^[0-9a-f]{6}$/.test(value);
}

Routing Groups

Some routes need authentication. Add a subfolder for pages under (authed):

js
// src/routes/(authed)/+layout.server.js
import { redirect } from '@sveltejs/kit';
export function load({ cookies, url }) {
if (!cookies.get('logged_in')) {
throw redirect(303, `/login?redirectTo=${url.pathname}`);
}
}
// src/routes/(authed)/+layout.svelte
<form method="POST" action="/logout">
<button>Log out</button>
</form>
js
// src/routes/login/+page.server.js
export const actions = {
default: ({ cookies, url }) => {
cookies.set('logged_in', 'true', { path: '/' });
throw redirect(303, url.searchParams.get('redirectTo') ?? '/');
}
};
js
// src/routes/logout/+page.server.js
export const actions = {
default: ({ cookies }) => {
cookies.delete('logged_in', { path: '/' });
throw redirect(303, '/');
}
};

Layout Break

src/routes/a/b/c/+page.svelte inherits four layouts:

  • src/routes/+layout.svelte
  • src/routes/a/+layout.svelte
  • src/routes/a/b/+layout.svelte
  • src/routes/a/b/c/+layout.svelte

Rename to +page@[level].svelte to put the page inside routes/.../[level].

Note: The root layout applies to every page of your app; you cannot break out of it.


Data Loading

  • +page.js and +layout.js files export universal load functions that run both on the server and in the browser.
  • +page.server.js and +layout.server.js files export server load functions that only run server-side.

Server load functions are convenient when you need to access data directly from a database or filesystem, or need to use private environment variables.

Universal load functions are useful when you need to fetch data from an external API and don't need private credentials, since SvelteKit can get the data directly from the API rather than going via your server. They are also useful when you need to return something that can't be serialized, such as a Svelte component constructor (not instance).

In rare cases, you might need to use both together — for example, you might need to return an instance of a custom class that was initialized with data from your server.

js
// src/routes/+page.server.js
export async function load() {
return {
message: 'this data came from the server',
cool: false
};
}
js
// src/routes/+page.js
export async function load({ data }) {
const module = data.cool
? await import('./CoolComponent.svelte')
: await import('./BoringComponent.svelte');
return {
component: module.default,
message: data.message
};
}

Load dynamic component:

<script>
export let data;
</script>
<svelte:component this={data.component} message={data.message} />

Using Parent Data

js
// parent: src/routes/+layout.server.js
export function load() {
return { a: 1 };
}
js
// child: src/routes/sum/+layout.js
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
}
js
// child page: src/routes/sum/+page.js
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
}
<script>
export let data;
</script>
<p>{data.a} + {data.b} = {data.c}</p>
<p><a href="/">home</a></p>

Using Child Data

A parent layout might need to access page data or data from a child layout.

<script>
import { page } from '$app/stores';
</script>
<svelte:head>
<title>{$page.data.title}</title>
</svelte:head>

Invalidate/Reload Route

With the same URL, Kit only runs once for optimization, but the data may change over time.

js
onMount(() => {
const interval = setInterval(() => {
invalidate('/api/now'); // route
}, 1000);
return () => {
clearInterval(interval);
};
});
// invalidate(...) takes a URL and re-runs any load functions that depend on it.

Manual Invalidation/Dependency

js
export async function load({ fetch, depends }) {
// load reruns when `invalidate('https://api.example.com/random-number')` is called...
const response = await fetch('https://api.example.com/random-number');
// ...or when `invalidate('app:random')` is called
depends('app:random');
return {
number: await response.json()
};
}
<script>
import { invalidate, invalidateAll } from '$app/navigation';
/** @type {import('./$types').PageData} */
export let data;
function rerunLoadFunction() {
// any of these will cause the `load` function to re-run
invalidate('app:random');
invalidate('https://api.example.com/random-number');
invalidate(url => url.href.includes('random-number'));
invalidateAll();
}
</script>

When Do Load Functions Re-run?

A load function will re-run in the following situations:

  • It references a property of params whose value has changed
  • It references a property of url (such as url.pathname or url.search) whose value has changed. Properties in request.url are not tracked
  • It calls await parent() and a parent load function re-ran
  • It declared a dependency on a specific URL via fetch (universal load only) or depends, and that URL was marked invalid with invalidate(url)
  • All active load functions were forcibly re-run with invalidateAll()

params and url can change in response to a <a href=".."> link click, a <form> interaction, a goto invocation, or a redirect.

Note: Re-running a load function will update the data prop inside the corresponding +layout.svelte or +page.svelte; it does not cause the component to be recreated. As a result, internal state is preserved. If this isn't what you want, you can reset whatever you need to reset inside an afterNavigate callback, and/or wrap your component in a {#key ...} block.


Environment Variables

  • $env/dynamic/private
  • $env/static/private
  • $env/static/public
  • $env/dynamic/public

Static vs Dynamic

The static in $env/static/private indicates that these values are known at build time, and can be statically replaced.

Static variables get replaced at build time and dynamic variables get replaced at runtime. Static variables allow compile-time computations which can give better performance (e.g., if they're used in an if condition which contains expensive code inside such as a dynamic import). But for the most part, the question is really just about when you want to set the variable — buildtime or runtime.


Locals

The interface that defines event.locals, which can be accessed in hooks (handle and handleError), server-only load functions, and +server.js files.

ts
interface Locals {}

js
import { afterNavigate } from '$app/navigation';
// If we came from /posts, we will use history to go back to preserve state
let canGoBack = false;
afterNavigate(({ from }) => {
if (from && from.url.pathname.startsWith('/posts')) {
canGoBack = true;
}
});
function goBack() {
if (canGoBack) {
history.back();
}
}