SvelteKit Advanced
Advanced SvelteKit features reference. Covers state management across server/client, the remote functions API, env vars (legacy and explicit), hooks (server/shared/universal), error handling, link options, service workers, server-only modules, snapshots, shallow routing, and the $app/* modules.
Source: official SvelteKit llms.txt documentation.
When to use this skill
Use this skill when you need to:
- Choose where state lives (server context, URL, snapshot, store).
- Build type-safe client/server RPC with
query/form/command/prerender. - Read env vars safely (private vs public, static vs dynamic).
- Customize request handling via hooks (
handle,handleFetch,handleError,handleValidationError,reroute,transport,init). - Throw expected/unexpected errors, customize the fallback error page, type error shape via
App.Error. - Configure link behavior with
data-sveltekit-*attributes. - Add a service worker for offline / precaching.
- Prevent secret leak via
.server.*or$lib/server/. - Persist ephemeral DOM state with snapshots or route history entries with shallow routing.
- Use the
$app/*modules (forms,navigation,state,paths,server,environment,types).
Do NOT use this skill for routing, form actions (legacy), load, basic setup, adapters configuration beyond env vars, or CSS/styling.
Critical sections
1. State management
- No shared server state — module-level variables (
let user;) leak across requests. Authenticate via cookies, persist to DB. - Pure
loadfunctions — no side effects (no global stores). Return data instead. - Context for per-request state —
setContext('user', () => data.user)(pass a function so reactivity crosses boundaries). Reading context updated during SSR in a child does NOT propagate to the parent (it has already rendered). - Component state is preserved across nav —
constderived fromdataonly computes once. Use$derived(...)for values that should recompute when data changes. - URL state — search params for filters/sort; survives reload, affects SSR.
- Snapshots — disposable UI state (e.g., "is accordion open?") bound to history entry.
2. Remote functions (since 2.27, experimental)
Opt in via svelte.config.js:
kit: { experimental: { remoteFunctions: true } },
compilerOptions: { experimental: { async: true } }
Flavors exported from *.remote.js:
| Flavor | Purpose | Key features |
|---|---|---|
query |
Read dynamic server data | Dedup, refresh(), loading/error/current |
query.batch |
Batch n+1 queries in one call | Returns (input, idx) => Output |
query.live |
Real-time async iterable | connected, reconnect(); first value serialized for SSR |
form |
Progressive-enhanced <form> |
<form {...createPost}>, createPost.fields.x.as('text'), validate(), enhance(), for(id), preflight(schema) |
command |
Imperative mutation from event handlers | Cannot be called during render |
prerender |
Build-time data | inputs, dynamic: true |
- Validate with any Standard Schema (Zod/Valibot).
- Args/returns serialized via devalue.
getRequestEvent()works inside remote functions for cookies.redirect(...)allowed inquery/form/prerender, NOT incommand.- Single-flight mutations:
getPosts().refresh()orgetPost(id).set(...)in server handler;requested(getPosts, 1).refreshAll()for client-requested refreshes (limit is DoS protection).
3. Environment variables
Legacy $env/* (default before SvelteKit 2.63):
| Module | Scope | Timing |
|---|---|---|
$env/dynamic/private |
Server only | Runtime (process.env-like) |
$env/dynamic/public |
Public (PUBLIC_*) |
Runtime |
$env/static/private |
Server only | Build time (inlined) |
$env/static/public |
Public | Build time (inlined) |
Explicit env vars (opt-in, default in v3):
Enable in svelte.config.js: kit.experimental.explicitEnvironmentVariables = true. Then create src/env.ts:
import { defineEnvVars } from '@sveltejs/kit/hooks';
import * as v from 'valibot';
import { building } from '$app/env';
export const variables = defineEnvVars({
API_KEY: {}, // private
GOOGLE_ANALYTICS_ID: { public: true }, // public
SHOW_DEBUG_OVERLAY: { public: true, static: true }, // inlined, dead-code-eliminated
SECRET: { schema: building ? v.optional(v.string()) : v.string() },
CACHE_TTL_SECONDS: { description: 'How long...' }
});
Import from $app/env/private or $app/env/public. $app/environment is renamed to $app/env.
4. Hooks
Three optional files: src/hooks.server.js, src/hooks.client.js, src/hooks.js.
Server (hooks.server.js):
handle({ event, resolve })— runs on every request; return Response or callresolve(event, opts).resolveopts:transformPageChunk,filterSerializedResponseHeaders,preload. Usesequence(...)for multiple.handleFetch({ request, fetch, event })— rewrite cross-origin requests to internal APIs; cookie forwarding for sibling subdomains.handleValidationError({ event, issues })— customize 400 response for bad remote function args.
Shared (hooks.server.js AND hooks.client.js):
handleError({ error, event, status, message })— for unexpected errors only. Return{ message, ... }-> becomespage.error. Must never throw. Server type:HandleServerError; client type:HandleClientError; clienteventisNavigationEvent.init()— runs once at startup; useful for DB connections.
Universal (hooks.js):
reroute({ url, fetch })— translate URL to a different route (e.g., i18n). Pure/idempotent. Can be async since 2.18.transport— custom encoders/decoders for types crossing the server/client boundary (e.g.,Vector).
5. Errors
- Expected errors —
error(404, 'Not found')(orerror(404, { message, code })) from@sveltejs/kit. Renders nearest+error.svelte, sets status code.page.error= the object passed. - Unexpected errors — any other exception. Not exposed (generic
Internal Error); routed throughhandleError. - Rendering errors — opt in via
experimental.handleRenderingErrors. Error passed directly to+error.svelteaserrorprop (not viapage.error). - Responses — custom
src/error.htmlwith%sveltekit.status%and%sveltekit.error.message%. Errors in root+layout.server.jsuse fallback page (root contains+error.svelte). - Type safety — declare
App.Errorinterface insrc/app.d.ts:
declare global {
namespace App {
interface Error { message: string; code: string; id: string; }
}
}
6. Link options
data-sveltekit-* attributes on <a> (or parent). Also apply to <form method="GET">.
| Attribute | Values | Effect |
|---|---|---|
preload-data |
hover (default), tap |
Preload load data on hover/tap |
preload-code |
eager, viewport, hover, tap |
Preload route code only |
reload |
boolean | Force full-page nav (also rel="external") |
replacestate |
boolean | Replace history entry instead of push |
keepfocus |
boolean | Keep focus on the same element after nav |
noscroll |
boolean | Disable scroll-to-top after nav |
Disable in subtree with data-sveltekit-preload-data="false". Respects navigator.connection.saveData.
7. Service workers
Place src/service-worker.js (or src/service-worker/index.js) — bundled and auto-registered. Disable via config to register manually.
Available from $service-worker: base, build, files, prerendered, version.
Standard pattern: cache build + files on install, network-first with cache fallback on fetch. Skip responses with Cache-Control: no-store (live queries). build/prerendered are empty arrays in dev.
8. Server-only modules
$env/static/privateand$env/dynamic/private— server-only.$app/server— server-only.- Your modules — add
.server.jssuffix OR place under$lib/server/.
Illegal imports from browser code error at build. Use import type for type-only. Detection is disabled in tests (process.env.TEST === 'true').
9. Snapshots & shallow routing
Snapshots — export snapshot = { capture, restore } from +page.svelte or +layout.svelte. Captured to sessionStorage before page updates; restored on history nav. Must be JSON-serializable. Don't return huge objects.
Shallow routing — pushState(url, state) / replaceState(url, state) create history entries without navigating. Read via page.state. Use preloadData(href) to grab load data, then pushState(href, { selected: result.data }) to render another +page.svelte inside a modal. page.state is always {} on first SSR and during first paint.
Quick Fixes
| Symptom | Fix |
|---|---|
| User data leaks between requests | Don't use module-level vars; use cookies + DB |
load data not updating on nav |
Use $derived(...) not const for values derived from data |
Cannot import $lib/server/... |
Move shared types to import type |
| Live query stream stops | Reconnect with .reconnect(); don't cache no-store responses in SW |
error(404, ...) shows blank page |
Add +error.svelte nearest to the route |
| Server-only env leaked to client | Use $env/static/private or .server naming |
| Validation 400 generic message | Implement handleValidationError |
| Component state lost on nav | Wrap with {#key page.url.pathname}<X/>{/key} to force remount |
Gotchas
loadmust be pure — no global store writes. Return the data instead.- Context updates during SSR don't propagate up — pass state down to avoid flash on hydration.
query.batchreturns a single function that maps individual inputs to outputs; not a direct array of results.query.liveon SSR returns ONLY the first yielded value then closes.commandcannot be called during render — invoke from event handlers.requestedrequires alimit— DoS protection; passInfinityonly if explicitly safe.error()no longer needsthrowin SvelteKit 2.x.page.stateis{}on SSR and first paint — don't rely on it for critical render.reroutemust be pure/idempotent — its result is cached per URL on the client.- Service worker
build/prerenderedempty in dev — test in production build. - Cookie forwarding for sibling subdomains requires manual
handleFetch— SvelteKit can't tell which parent-domain cookie belongs to which subdomain. handleErrormust not throw — wrap risky work in try/catch.
FAQ
Q: $app/state vs $app/stores?
A: $app/state (since 2.12) is rune-based and reactive. $app/stores is the legacy store-based version. Use $app/state with Svelte 5.
Q: query vs prerender?
A: query for dynamic data; prerender for build-time-frozen data that can be served from a CDN. query cannot be used on a fully prerendered page.
Q: form vs command?
A: form is progressive-enhanced — works without JS, spreads onto <form>. command is JS-only, called from event handlers. Prefer form.
Q: $env/dynamic vs $env/static?
A: dynamic reads at runtime (e.g., process.env); static inlined at build time (enables dead-code elimination). Use static for build-time-known values.
Q: $env/static/private vs $env/dynamic/private?
A: Same access restriction; difference is when the value is read. Static gives DCE; dynamic allows runtime override (e.g., MY_FLAG=1 npm run dev).
Q: Can I use $lib/server modules from +page.svelte? A: No — server-only modules cannot be imported by client code, transitively. SvelteKit errors at build.
Q: How do I keep focus on a search input after submit?
A: Add data-sveltekit-keepfocus to the <form>.
Q: How do I throw a 404 from a load?
A: import { error } from '@sveltejs/kit'; error(404, 'Not found'); (don't throw in SvelteKit 2).
Q: Should I use Sentry?
A: Yes — initialize in handleError of both hooks.server.js and hooks.client.js. Server uses HandleServerError; client uses HandleClientError with NavigationEvent.
Examples
| Topic | File |
|---|---|
| State management with context + URL + derived | examples/state-management.md |
| Remote query/form/command/batch/live | examples/remote-functions.md |
| Env vars (legacy + explicit) | examples/env-vars.md |
| Server + universal hooks | examples/hooks.md |
| Expected/unexpected errors + App.Error | examples/errors.md |
| data-sveltekit-* link options | examples/link-options.md |
| Service worker precache + offline | examples/service-worker.md |
| $app/* modules (forms, navigation, state, paths, server) | examples/app-modules.md |
| Snapshots + shallow routing | examples/shallow-snapshots.md |
References
| Topic | File |
|---|---|
| State management | references/state-management.md |
| Remote functions | references/remote-functions.md |
| Environment variables | references/env-vars.md |
| Hooks | references/hooks.md |
| Errors | references/errors.md |
| Link options | references/link-options.md |
| Service workers | references/service-worker.md |
| Server-only modules | references/server-only.md |
| $app/* modules | references/app-modules.md |
| Snapshots & shallow routing | references/shallow-snapshots.md |