sveltekit
SvelteKit
Topics
| Topic | File |
|---|---|
| Routing, layouts, error boundaries, SSR | structure.md |
| Load functions, form actions, serialization | data-flow.md |
| Authentication, hooks, route protection | auth.md |
| Form validation, extractFormData, FormErrors | forms-validation.md |
| Remote functions (command/query/form) | remote-functions.md |
Structure & Routing
File types: +page.svelte (page) | +layout.svelte (wrapper) | +error.svelte (error boundary) | +server.ts (API endpoint)
Routes: src/routes/about/+page.svelte → /about | src/routes/posts/[id]/+page.svelte → /posts/123
Layouts apply to all child routes. Use (groups) for layout organization without affecting URLs.
src/routes/
├── +layout.svelte # Root layout (all pages)
├── +page.svelte # Homepage /
├── (app)/ # Protected routes (group doesn't affect URL)
│ ├── +layout.server.ts # Auth check for all (app) routes
│ ├── +layout.svelte # Nav bar, user info
│ └── dashboard/+page.svelte # /dashboard
└── (auth)/ # Public routes
└── login/+page.svelte # /login
<!-- +layout.svelte — must render children in Svelte 5 -->
<script>
let { children } = $props();
</script>
<nav><!-- Navigation --></nav>
<main>{@render children()}</main>
→ Deep dives: structure.md for file naming, layout nesting, error boundaries, SSR/hydration.
Data Loading
Which file? Server-only (DB, secrets) → +page.server.ts | Universal (both sides) → +page.ts | API → +server.ts
// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
export const load = async ({ locals }) => {
const user = await db.users.get(locals.userId);
return { user }; // Must be JSON-serializable
};
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) return fail(400, { email, missing: true });
await updateEmail(email);
throw redirect(303, '/success');
},
};
Key rules:
- Always
throw redirect()andthrow error()— in SvelteKit 2 these return objects, they don't throw automatically - Server load output is automatically passed to universal load as
dataparameter - Don't return class instances or functions from server load (not serializable)
- Form actions always go in
+page.server.ts
→ Deep dives: data-flow.md for load functions, form actions, serialization rules.
Authentication
Route protection happens in layout server files, NOT in hooks.
Hooks populate locals.session/locals.user. Layouts check and redirect.
// routes/(app)/+layout.server.ts
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.session) {
throw redirect(303, '/login'); // ⚠️ Must throw, not bare call
}
return { user: locals.user };
};
⚠️ Layouts do NOT protect API routes. You must check locals.session explicitly in every +server.ts:
// routes/(app)/api/data/+server.ts
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.session) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
// ...
};
⚠️ Never silently skip auth in hooks — if your DB binding is missing, throw new Error(), don't return resolve(event). Silent fallthrough means unauthenticated users get through.
→ Deep dives: auth.md for full hooks setup, TypeScript config, Better Auth and Cloudflare specifics.
Form Validation
Server-first validation with progressive enhancement. Schema defined once, server validates, client displays errors.
// $lib/schemas/profile.ts
import * as v from 'valibot';
export const ProfileSchema = v.object({
name: v.pipe(v.string(), v.trim(), v.minLength(1, 'Name is required.')),
email: v.pipe(v.string(), v.trim(), v.toLowerCase(), v.email('Invalid email.')),
});
Pattern: extractFormData() utility validates request → returns typed data or field-keyed errors → fail(400, { errors }) → client FormErrors class clears errors on input, shows after submit.
→ Deep dives: forms-validation.md for the full extractFormData utility, FormErrors class, cross-field validation, Field.Set, blur validation, and Zod equivalents.
Remote Functions
*.remote.ts files expose server functions callable from the browser:
// actions.remote.ts
import { command } from '$app/server';
import * as v from 'valibot';
export const delete_user = command(
v.object({ id: v.string() }),
async ({ id }) => {
await db.users.delete(id);
return { success: true };
},
);
// Client: await delete_user({ id: '123' });
Which function? One-time action → command() | Repeated reads → query() | Forms → form()
Args and returns must be JSON-serializable. Use getRequestEvent() for cookies/headers.
→ Deep dives: remote-functions.md for complete patterns.
Common Mistakes
| Mistake | Fix |
|---|---|
redirect() without throw |
throw redirect(303, '/path') |
| Protecting API routes via layouts | Check locals.session in each +server.ts |
| Silently skipping auth when DB missing | throw new Error() in hooks |
| Auth page inside protected group | Put login in (auth)/, not (app)/ |
| Returning non-serializable from load | Only return plain objects, no classes/functions |
<slot /> in layouts |
{@render children()} (Svelte 5) |
Reference Index
Structure: File Naming · Layout Patterns · Error Handling · SSR & Hydration
Data Flow: Load Functions · Form Actions · Serialization · Error & Redirect Handling
Auth: Better Auth · Cloudflare
Remote Functions: Reference