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 from a SvelteKit tutorial, helping developers quickly reference and apply their knowledge:

File Structure

server side
+layout.svelte share common js using </slot>
+layout.server.js load data for every child route
+server.js api only (and don't need to have +page.svelte)

client side
+page.server.js load data and handle api request
+page.js: dynamic component
+page.svelte content

Setting cookies

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


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

Shared module


  • use $lib to access module instead of ../../


Default form

input from user'

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

Named form actions

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">
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">
Notice that we haven't had to write any fetch code or anything like that β€”data updates automatically.

Validation and Throw Error

server side

// server side
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, access the returned value via the form prop

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

Progressive Enhancement

  • run js code when submitting a form, useful when a page have many actions within the same url
  • enhance the element behaviors using JS without reloading when updating the page
import { enhance } from '$app/forms';
// add the use:enhance directive to the <form> elements:
<form method="POST" action="?/create" use:enhance={func}>


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;
<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


  • use +server.js files to expose (for example) a JSON API
// +page.svelte
function rerun() {
fetch('/api/ci', {
method: 'POST'
<button on:click={rerun}>Rerun CI</button>
// +server.js
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something


// 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:, done });
return new Response(null, { status: 204 });
export async function DELETE({ params, cookies }) {
const userid = cookies.get('userid');
await database.deleteTodo({ userid, id: });
return new Response(null, { status: 204 });
// src/routes/+page.svelte
await fetch(`/todo/${}`, {
method: 'PUT', // <-------------------
body: JSON.stringify({ done }),
headers: {
'Content-Type': 'application/json'

Built-in Stores

  • access route
import { page, navigating, updated } from '$app/stores';
// $page.url.pathname
// $
  • handling page version
{#if $updated}
<p class="toast">
A new version of the app is available
<button on:click={() => location.reload()}>
reload the page

Error Handling

Throwing error

  • error vs Error
  • Expected error (no log and stack trace)
import { error } from '@sveltejs/kit';
throw error(420, 'enhance your calm');
  • Unexpected error (a bug in the app, (have log and stack trace))
throw new Error('Kaboom!');

error page



import { redirect } from '@sveltejs/kit';
export function load() {
throw redirect(307, '/b');
// 303 β€” for form actions, following a successful submission
// 307 β€” for temporary redirects
// 308 β€” for permanent redirects
export function handleLoginRedirect(
message = "You must be logged in to access this page"
) {
const redirectTo = event.url.pathname +
return `/login?redirectTo=${redirectTo}&message=${message}`
export const load = async (event) => {
if (!event.locals.user) {
throw redirect(302, handleLoginRedirect(event))

Goto in client side

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


  • middleware
  • intercept and override the framework's default behaviour.

Intercepting pages

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

Intercepting fetches

// it can be used to make credentialed requests on the server
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

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

export const ssr = false;
export const csr = false;
This means that no JavaScript is served to the client
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).

<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)

"eager" β€” preload everything on the page following a navigation
"viewport" β€” preload everything as it appears in the viewport
"hover" (default) as above
"tap" β€” as above
"off" β€” as above

Preloading programmatically

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


SvelteKit is holding the page Snapshots even switch pages to disable this behaviour. You can do so by adding the data-sveltekit-reload attribute on an individual link

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


Optional route parameters


const greetings = {
en: 'hello!',
de: 'hallo!',
fr: 'bonjour!'
export function load({ params }) {
return {
greeting: greetings[params.lang ?? 'en']

Route regular expression match


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

Routing group

some routes need auth, add the subfolder for pages under (authed)

// 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>
// 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') ?? '/');
// 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:


rename `+page@[level].svelte`
would put the page inside routes/.../[level]
** 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 initialised with data from your server.

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

Using parent data

// parent: src/routes/+layout.server.js
export function load() {
return { a: 1 };
// child: src/routes/sum/+layout.js
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
// child page: src/routes/sum/+page.js
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
export let data;
<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
import { page } from '$app/stores';

Invalidate /reload route (e.g. api)

// with the same URL, Kit only run once for optimization
// but the data may change over time
onMount(() => {
const interval = setInterval(() => {
invalidate('/api/now'); // <---route
}, 1000);
return () => {
// invalidate(...)
// takes a URL and re-runs any load functions that depend on it.

Manual invalidation/dependency

export async function load({ fetch, depends }) {
// load reruns when `invalidate('')` is called...
const response = await fetch('');
// ...or when `invalidate('app:random')` is called
return {
number: await response.json()
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(url => url.href.includes('random-number'));

When do load functions re-run?
To summarize, 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 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 that 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

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.

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

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