web-meta-framework-sveltekit · git:20260906.ae0cc61 · 2026-09-06 · sha256 e2818e0ddd1cbf47
web-meta-framework-sveltekit git:20260906.ae0cc61B
Immutable. This exact content is served forever at /api/v1/blob/e2818e0ddd1cbf47.
---
name: web-meta-framework-sveltekit
description: SvelteKit full-stack framework - file-based routing, load functions, form actions, server hooks, SSR/SSG, API routes, streaming, progressive enhancement
---
# SvelteKit Patterns
> **Quick Guide:** Load functions fetch, form actions mutate, `+server.ts` serves external clients,
> and hooks handle what applies to every request. Data reaches a component as the `data` prop and an
> action's result as the `form` prop, both typed from the generated `$types`. Two facts change most
> answers below: `+page.server.ts` runs only on the server while `+page.ts` also runs in the browser,
> and `fail()` returns while `error()` and `redirect()` throw.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — file conventions, dynamic routes, route groups, parameter matchers, error boundaries
- [examples/load-functions.md](examples/load-functions.md) — server and universal loads, layout data, streaming, `parent()`, invalidation
- [examples/form-actions.md](examples/form-actions.md) — default and named actions, `fail()`, `use:enhance`, redirect ordering
- [examples/hooks.md](examples/hooks.md) — `handle`, `handleFetch`, `handleError`, `init`, `reroute`, `transport`, `sequence`
- [examples/api-routes.md](examples/api-routes.md) — `+server.ts` verbs, streaming responses, uploads, content negotiation
- [reference.md](reference.md) — decision trees, load-function inputs, the import surface, page options
---
## Which path applies
- **Reading data for a page** — the choice is `+page.server.ts` versus `+page.ts`, and it turns on
whether the code may run in the browser; follow
[examples/load-functions.md](examples/load-functions.md).
- **Writing data from a page** — a form action, not an API route; follow
[examples/form-actions.md](examples/form-actions.md).
- **Serving something that is not a page** — a `+server.ts` route with one export per HTTP verb;
follow [examples/api-routes.md](examples/api-routes.md).
- **Anything that applies to every request** — auth, headers, logging, URL rewriting; follow
[examples/hooks.md](examples/hooks.md).
---
<critical_requirements>
## Before writing SvelteKit code
**Put anything touching a database, a secret or a cookie in `+page.server.ts`.** A `+page.ts` load
also runs in the browser, so whatever it imports is in the client bundle.
**Handle mutations with form actions rather than API routes.** An action gets progressive
enhancement, CSRF protection and automatic revalidation; a `fetch` to a `+server.ts` route gets none
of them for free.
**Return `fail(status, data)` for a validation failure, and put the values the user typed in it.**
`fail` populates the `form` prop, so the page re-renders with the input intact — `error()` throws to
`+error.svelte` and the form is gone.
**Validate on the server even where the browser already checked.** `required` and `type="email"` are
a courtesy to the user; a POST can arrive without passing through the form at all.
**Type load functions and page props from the generated `./$types`.** `PageServerLoad`, `PageProps`
and `LayoutProps` are derived from the route's own files, so a renamed parameter becomes a compile
error.
**Add `use:enhance` to every form.** Without it a submission is a full page reload, which still works
and is much slower.
**Call `redirect()` outside any `try`.** It signals by throwing, so a surrounding `catch` swallows
the navigation and reports a failure that did not happen.
</critical_requirements>
---
**Auto-detection:** +page.svelte, +page.ts, +page.server.ts, +layout.svelte, +layout.ts,
+layout.server.ts, +error.svelte, +server.ts, hooks.server.ts, hooks.client.ts, hooks.ts, load
function, form actions, use:enhance, handle, handleFetch, handleError, handleValidationError, init,
reroute, transport, sequence, $app/navigation, $app/forms, $app/state, $env/static/private,
$env/dynamic/public, PageServerLoad, PageLoad, LayoutServerLoad, RequestHandler, PageProps,
LayoutProps, fail, redirect, error, invalidate, invalidateAll, depends, event.locals, .remote.ts
**Applies to:**
- File-based routing with layouts, error boundaries, route groups and parameter matchers
- Loading data on the server, universally, or streamed in behind the first paint
- Form submissions that work before hydration and better after it
- API endpoints, streaming responses and file uploads through `+server.ts`
- Cross-cutting request handling in hooks: sessions, headers, URL rewriting, error reporting
- Choosing per route between prerendering, server rendering and client-only rendering
**Handled elsewhere:**
- Svelte component authoring itself — runes, snippets, event handling and reactivity
- Persistence — a load function reads and an action writes; neither the client nor the query shape
is settled here
- Session issuance and password handling — this skill's examples call an auth layer and read
`locals`
- Schema validation — an action parses `FormData`; which library defines the schema is a separate
choice
- Styling, beyond where a stylesheet is imported
---
<philosophy>
## Philosophy
SvelteKit is built on web platform objects rather than framework abstractions: `Request`, `Response`,
`URL`, `Headers` and `FormData` are the whole vocabulary. That is why a form works without
JavaScript, why caching is a `Cache-Control` header, and why an error is a status code.
The framework's shape is four roles, each with one job:
```
Request → hooks.server.ts (handle) → +layout.server.ts (load) → +page.server.ts (load) → +page.svelte
← form actions (POST)
```
1. **Hooks** own what is true of every request
2. **Load functions** own reads, and run in parallel with each other rather than down the tree
3. **Form actions** own writes, and revalidate the loads afterwards
4. **Components** own rendering, and receive `data` and `form` as props
Type safety is generated rather than declared: `$types` is derived from the file tree, so the types
follow a renamed route without anyone updating them.
</philosophy>
---
<patterns>
## Core patterns
### Pattern 1: File Conventions
Directories under `src/routes/` are URL segments; the `+` files decide what each segment does.
| File | Purpose | Runs on |
| ------------------- | ---------------------------------- | --------------------- |
| `+page.svelte` | The page itself | Server (SSR) + client |
| `+page.ts` | Universal load | Server + client |
| `+page.server.ts` | Server load and form actions | Server only |
| `+layout.svelte` | Wrapper for the segment and below | Server (SSR) + client |
| `+layout.ts` | Universal layout load | Server + client |
| `+layout.server.ts` | Server layout load | Server only |
| `+error.svelte` | Error boundary for the segment | Server (SSR) + client |
| `+server.ts` | HTTP endpoint, one export per verb | Server only |
Bracket depth chooses the segment kind — `[slug]`, `[...path]`, `[[lang]]` — and `(name)` groups
routes under a shared layout without appearing in the URL.
Full code: [examples/core.md](examples/core.md)
### Pattern 2: Server Load Functions
The default choice: it can read the database, the cookies and the private environment, because
nothing in it reaches the browser.
```typescript
// src/routes/blog/+page.server.ts
export const load: PageServerLoad = async ({ url, locals }) => {
if (!locals.user) error(401, "Not authenticated");
const page = Number(url.searchParams.get("page") ?? "1");
const [posts, total] = await Promise.all([listPosts(page), countPosts()]);
return { posts, pagination: { page, total } };
};
```
Full code: [examples/load-functions.md](examples/load-functions.md)
### Pattern 3: Universal Load Functions
`+page.ts` runs on the server for the first request and in the browser for every navigation after,
so it suits a public API and nothing that needs a secret.
```typescript
// src/routes/weather/+page.ts
export const load: PageLoad = async ({ fetch, params }) => {
const response = await fetch(
`https://api.example.com/forecast/${params.city}`,
);
if (!response.ok) error(response.status, "Failed to load forecast");
return { forecast: await response.json() };
};
```
That `fetch` is SvelteKit's own: it inherits cookies, deduplicates, and calls an internal
`+server.ts` route directly on the server rather than over HTTP.
Full code: [examples/load-functions.md](examples/load-functions.md)
### Pattern 4: Layout Loads
Data returned from a layout load is available to every page beneath it, which makes a layout the
natural place for an auth check.
```typescript
// src/routes/dashboard/+layout.server.ts
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) redirect(303, "/login");
return { user: locals.user, notifications: await unreadFor(locals.user.id) };
};
```
A child reaches it with `await parent()` — called after its own independent queries have started,
since `parent()` blocks.
Full code: [examples/load-functions.md](examples/load-functions.md)
### Pattern 5: Form Actions
Actions live beside the load in `+page.server.ts`. A page has either one `default` action or any
number of named ones.
```typescript
export const actions: Actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get("email")?.toString() ?? "";
if (!email) return fail(400, { email, message: "Email is required" });
cookies.set("session", await createSession(email), { path: "/" });
redirect(303, "/dashboard"); // outside any try — it throws
},
};
```
```svelte
<form method="POST" action="?/login" use:enhance>
```
`?/login` targets the named action; the returned `fail` payload arrives as the `form` prop, which is
what refills the inputs.
Full code: [examples/form-actions.md](examples/form-actions.md)
### Pattern 6: Streaming
Await what the page cannot render without and return the rest unawaited; `{#await}` covers the three
states in the markup.
```typescript
export const load: PageServerLoad = async ({ locals }) => {
const user = await getUser(locals.user.id); // blocks the first paint
return {
user,
analytics: getAnalytics(locals.user.id), // streams in later
};
};
```
```svelte
{#await data.analytics}
<div class="skeleton">Loading…</div>
{:then analytics}
<p>Views: {analytics.views}</p>
{:catch error}
<p role="alert">Failed to load analytics</p>
{/await}
```
Only a server load can stream — the values have to be serialisable.
Full code: [examples/load-functions.md](examples/load-functions.md)
### Pattern 7: Errors
`error(status, message)` throws to the nearest `+error.svelte`, which reads `page` from `$app/state`.
```svelte
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<h1>{page.status}</h1>
<p>{page.error?.message ?? 'Something went wrong'}</p>
```
The boundary walks up the tree, so an `+error.svelte` in a segment keeps the failure inside it.
Full code: [examples/core.md](examples/core.md)
### Pattern 8: Hooks
`hooks.server.ts` runs for every request — the single place to establish who the caller is.
```typescript
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get("session");
event.locals.user = sessionId ? await getUserFromSession(sessionId) : null;
return resolve(event);
};
```
`event.locals` is request-scoped, so every load function and action downstream reads the same user.
`sequence()` composes several hooks; `handleFetch`, `handleError`, `init`, `reroute` and `transport`
cover the rest.
Full code: [examples/hooks.md](examples/hooks.md)
### Pattern 9: Page Options
Three exports decide how a route is rendered, and they apply to the segment and everything under it.
```typescript
export const prerender = true; // static HTML at build time
export const ssr = false; // client-only rendering
export const csr = false; // no JavaScript shipped at all
```
Prerender static content, disable SSR for a page that needs browser APIs at first render, and
disable CSR for pages with no interactivity.
Full code: [reference.md](reference.md)
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- Database access or a private environment variable in `+page.ts` — the module is bundled for the
browser
- `redirect()` inside a `try` — it throws, so the `catch` reports a failure instead of navigating
- `fail(...)` called without `return` — it produces a value rather than throwing, so execution
continues
- A form with no `method="POST"` — a GET reaches the load function, and the action never runs
- A mutation reachable without an auth check — actions and `+server.ts` routes are public endpoints
- Non-serialisable data returned from a server load — a class or function cannot cross the boundary
unless a `transport` hook encodes it
- `event.locals` accessed without an `App.Locals` declaration in `app.d.ts` — the property is
untyped
**Surprising behaviour:**
- `error()` renders `+error.svelte` instead of the page, so the page component never runs at all
- `redirect()` after a POST should use 303, or the browser repeats the POST at the new URL
- Load functions for a route run concurrently, so an ordering assumption between two of them is
unfounded
- `await parent()` before an independent query serialises what could have been parallel
- Layout and page data merge rather than replace, and the page wins on a shared key
- A form without `use:enhance` reloads the whole page, which is correct behaviour rather than a bug
- `page` from `$app/stores` still works and is the pre-Svelte-5 form; `$app/state` is the current one
- A cookie set without `path: '/'` may not be sent back on the next request
- Remote functions (`.remote.ts`) are experimental behind `kit.experimental.remoteFunctions`
More gotchas, the load-function input matrix and the import surface are in
[reference.md](reference.md).
</red_flags>