SvelteKit (Svelte 5) — Claude Code rules
You are working in a SvelteKit app using Svelte 5 with runes. Prefer runes over the legacy Svelte 3/4 reactivity model. Stick to the conventions below.
Reactivity (runes)
- Declare reactive state with
$state(...). The value is a plain value you update like any variable — arrays/objects become a deeply reactive proxy, so mutating (arr.push(x),obj.foo = 1) triggers updates. - Use
$state.raw(...)when you do not want deep reactivity — raw state can only be reassigned, never mutated. - Pass reactive proxies to non-Svelte libraries via
$state.snapshot(value). - Compute values with
$derived(expr), or$derived.by(() => { ... })for multi-statement logic. The expression must be free of side-effects — Svelte disallows state changes (e.g.count++) inside it. - Runes are compiler keywords, not functions: don't import them, assign them to variables, or pass them as arguments.
Don't use legacy syntax
- Don't use
export letfor props — use$props(). - Don't use
$:reactive statements — use$derived(for values) or$effect(for side-effects). - Don't write Svelte stores (
writable/readable) for component-local state when runes do the job.
Effects
$effect(() => { ... })runs after the component mounts and re-runs (batched, in a microtask) after the state it synchronously reads changes. Values read afterawaitor insidesetTimeoutare not tracked.$effect.pre(...)runs before the DOM updates (e.g. measure-before-paint).$effectis an escape hatch for analytics, DOM manipulation, and external sync — not for deriving state. Don't use an effect to copy one piece of state into another; use$derivedinstead.- Don't read and write the same state in one effect (infinite loop); if unavoidable, wrap
the write in
untrack(...).
Components & props
- Read props with
$props():let { a, b = 'default', ...rest } = $props();. Rename withlet { class: className } = $props();. - Props are read-only — don't mutate props. Communicate up via callback props, or share a
value explicitly with
$bindable()for two-way binding. +layout.sveltemust render its children with{@render children()}.
Routing & data
- Routes are filesystem-based under
src/routes. A directory = a URL segment;[slug]is a dynamic param,[...rest]a rest param,[[optional]]an optional param. +page.svelterenders a page and receives loaded data via thedataprop (typedPageDatafrom./$types).+layout.sveltewraps child routes;+error.svelteis the error boundary.+server.jsdefines API endpoints — exportGET/POST/PUT/PATCH/DELETEreturning aResponse.- Load data in
loadfunctions, not in components:+page.server.js/+layout.server.js(server load) for DB/filesystem access, private env vars, cookies, andlocals. Returned data must be devalue-serializable (JSON,Date,Map,Set,BigInt, etc.).+page.js/+layout.js(universal load) runs on server then client; use for public API fetches and to return non-serializable values (e.g. component constructors).
- Use the injected
fetchinsideload(credential inheritance, relative URLs, SSR inlining). Readparams/url/route;await parent()for parent layout data. - Throw
redirect(3xx, location)anderror(4xx|5xx, message)from load. Declare custom invalidation deps withdepends(id)and refresh viainvalidate(id)/invalidateAll().
State management
- Never put per-user/request state in module-level variables on the server — modules are shared across requests and users, so state leaks between them.
loadfunctions must be pure: no side-effects, no writing to shared stores. Return data instead.- For shared state scoped to a request/tree, use
setContext/getContext, not globals. - Persist navigation-relevant state (filters, sort) in the URL search params; use snapshots for ephemeral UI state that should survive history navigation.