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
jsexport 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>jsexport const actions = { default: async ({ cookies, request }) => { const data = await request.formData(); db.createTodo(cookies.get('userid'), data.get('description')); }};Named Form Actions
jsexport 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 duplicatesif (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.
jsimport { enhance } from '$app/forms';Add the use:enhance directive to <form> elements:
<form method="POST" action="?/create" use:enhance={func}>SubmitFunction
tsfunction 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.jsimport * 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.svelteawait fetch(`/todo/${todo.id}`, { method: 'PUT', body: JSON.stringify({ done }), headers: { 'Content-Type': 'application/json' }});Built-in Stores
Access Route Information
jsimport { page, navigating, updated } from '$app/stores';// $page.url.pathname// $navigating.to.url.pathnameHandling 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):
jsimport { error } from '@sveltejs/kit';throw error(420, 'enhance your calm');Unexpected errors (a bug in the app, has log and stack trace):
jsthrow new Error('Kaboom!');Error Page
Create +error.svelte to handle errors.
Redirect
jsimport { 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 redirectsHelper function for login redirects:
jsexport 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.jsexport 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:
jsexport 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-renderedcsr— whether to load the SvelteKit clientprerender— whether to prerender pages at build time, instead of per-requesttrailingSlash— whether to strip, add, or ignore trailing slashes in URLs
jsexport const ssr = false;jsexport const csr = false;This means that no JavaScript is served to the client.
jsexport 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).
Link Options
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
jsimport { preloadCode, preloadData } from '$app/navigation';// preload the code and data needed to navigate to /foopreloadData('/foo');// preload the code needed to navigate to /bar, but not the datapreloadCode('/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.jsjsconst 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.jsexport 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.jsimport { 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.jsexport const actions = { default: ({ cookies, url }) => { cookies.set('logged_in', 'true', { path: '/' }); throw redirect(303, url.searchParams.get('redirectTo') ?? '/'); }};js// src/routes/logout/+page.server.jsexport 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.sveltesrc/routes/a/+layout.sveltesrc/routes/a/b/+layout.sveltesrc/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.jsand+layout.jsfiles export universal load functions that run both on the server and in the browser.+page.server.jsand+layout.server.jsfiles 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.jsexport async function load() { return { message: 'this data came from the server', cool: false };}js// src/routes/+page.jsexport 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.jsexport function load() { return { a: 1 };}js// child: src/routes/sum/+layout.jsexport async function load({ parent }) { const { a } = await parent(); return { b: a + 1 };}js// child page: src/routes/sum/+page.jsexport 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.
jsonMount(() => { 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
jsexport 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
paramswhose value has changed - It references a property of
url(such asurl.pathnameorurl.search) whose value has changed. Properties inrequest.urlare 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) ordepends, and that URL was marked invalid withinvalidate(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.
tsinterface Locals {}Navigation History
jsimport { afterNavigate } from '$app/navigation';// If we came from /posts, we will use history to go back to preserve statelet canGoBack = false;afterNavigate(({ from }) => { if (from && from.url.pathname.startsWith('/posts')) { canGoBack = true; }});function goBack() { if (canGoBack) { history.back(); }} 