0b76a4b119
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
16 KiB
16 KiB
SvelteKit Routing & Data Loading Guide
Overview
SvelteKit is the official framework for building Svelte applications. It provides file-based routing, server-side rendering, and powerful data loading primitives.
📁 File-Based Routing
Route Structure
src/routes/
├── +page.svelte # → /
├── +layout.svelte # Layout for all routes
├── about/
│ └── +page.svelte # → /about
├── blog/
│ ├── +page.svelte # → /blog
│ └── [slug]/
│ └── +page.svelte # → /blog/:slug
├── users/
│ ├── +page.svelte # → /users
│ ├── [id]/
│ │ ├── +page.svelte # → /users/:id
│ │ └── +page.server.ts # Server-only load
│ └── [...rest]/
│ └── +page.svelte # → /users/* (catch-all)
└── api/
└── posts/
└── +server.ts # → /api/posts (API endpoint)
Route Files Explained
| File | Purpose | Runs On |
|---|---|---|
+page.svelte |
Page component | Client |
+page.ts |
Load data for page | Client & Server |
+page.server.ts |
Load data (server only) | Server only |
+layout.svelte |
Shared layout | Client |
+layout.ts |
Load data for layout | Client & Server |
+layout.server.ts |
Load layout data (server) | Server only |
+server.ts |
API endpoint | Server only |
+error.svelte |
Error page | Client |
📥 Data Loading
Basic Load Function (+page.ts)
// src/routes/posts/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts = await response.json();
return {
posts
};
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>Posts</h1>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
{/each}
Server-Only Load (+page.server.ts)
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
import { error, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, cookies }) => {
// Access server-only resources
const session = cookies.get('session');
if (!session) {
redirect(303, '/login');
}
const user = await db.user.findUnique({
where: { sessionId: session }
});
if (!user) {
error(401, 'Unauthorized');
}
const stats = await db.stats.findMany({
where: { userId: user.id }
});
return {
user: {
name: user.name,
email: user.email
},
stats
};
};
Load with Parameters
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
if (!response.ok) {
error(404, 'Post not found');
}
const post = await response.json();
return {
post
};
};
Parent Data Access
// src/routes/+layout.ts
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async ({ fetch }) => {
const response = await fetch('/api/user');
const user = response.ok ? await response.json() : null;
return { user };
};
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
// Access data from parent layout
const { user } = await parent();
if (!user) {
// Handle unauthenticated state
}
return {
// Additional data
};
};
Parallel Data Loading
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
// Load data in parallel
const [postsRes, statsRes, notificationsRes] = await Promise.all([
fetch('/api/posts'),
fetch('/api/stats'),
fetch('/api/notifications')
]);
return {
posts: postsRes.json(),
stats: statsRes.json(),
notifications: notificationsRes.json()
};
};
📤 Form Actions
Basic Form Action
// src/routes/login/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ locals }) => {
if (locals.user) {
redirect(303, '/dashboard');
}
};
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email')?.toString();
const password = data.get('password')?.toString();
if (!email || !password) {
return fail(400, {
email,
error: 'Email and password are required'
});
}
const user = await db.user.findUnique({ where: { email } });
if (!user || !await verifyPassword(password, user.passwordHash)) {
return fail(401, {
email,
error: 'Invalid credentials'
});
}
const session = await createSession(user.id);
cookies.set('session', session.id, {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
});
redirect(303, '/dashboard');
}
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData, PageData } from './$types';
import { enhance } from '$app/forms';
let { form }: { form: ActionData } = $props();
let loading = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
loading = false;
await update();
};
}}
>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<label>
Email
<input
type="email"
name="email"
value={form?.email ?? ''}
required
/>
</label>
<label>
Password
<input type="password" name="password" required />
</label>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
Named Actions
// src/routes/settings/+page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
updateProfile: async ({ request }) => {
const data = await request.formData();
// Handle profile update
return { success: true };
},
changePassword: async ({ request }) => {
const data = await request.formData();
// Handle password change
return { success: true };
},
deleteAccount: async ({ request, cookies }) => {
// Handle account deletion
cookies.delete('session', { path: '/' });
redirect(303, '/');
}
};
<!-- Use named actions -->
<form method="POST" action="?/updateProfile" use:enhance>
<!-- Profile form -->
</form>
<form method="POST" action="?/changePassword" use:enhance>
<!-- Password form -->
</form>
<form method="POST" action="?/deleteAccount" use:enhance>
<button type="submit">Delete Account</button>
</form>
🔌 API Routes (+server.ts)
RESTful API Endpoint
// src/routes/api/posts/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
// GET /api/posts
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit')) || 10;
const offset = Number(url.searchParams.get('offset')) || 0;
const posts = await db.post.findMany({
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' }
});
return json(posts);
};
// POST /api/posts
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
error(401, 'Unauthorized');
}
const body = await request.json();
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
authorId: locals.user.id
}
});
return json(post, { status: 201 });
};
// src/routes/api/posts/[id]/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
// GET /api/posts/:id
export const GET: RequestHandler = async ({ params }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
return json(post);
};
// PUT /api/posts/:id
export const PUT: RequestHandler = async ({ params, request, locals }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
if (post.authorId !== locals.user?.id) {
error(403, 'Forbidden');
}
const body = await request.json();
const updated = await db.post.update({
where: { id: params.id },
data: body
});
return json(updated);
};
// DELETE /api/posts/:id
export const DELETE: RequestHandler = async ({ params, locals }) => {
const post = await db.post.findUnique({
where: { id: params.id }
});
if (!post) {
error(404, 'Post not found');
}
if (post.authorId !== locals.user?.id) {
error(403, 'Forbidden');
}
await db.post.delete({
where: { id: params.id }
});
return new Response(null, { status: 204 });
};
🏗️ Layouts
Root Layout
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import '../app.css';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
</script>
<div class="app">
<Header user={data.user} />
<main>
{@render children()}
</main>
<Footer />
</div>
Nested Layouts
<!-- src/routes/dashboard/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
</script>
<div class="dashboard">
<Sidebar />
<div class="content">
{@render children()}
</div>
</div>
Layout Groups
src/routes/
├── (auth)/ # Group - no route segment
│ ├── +layout.svelte # Shared layout for auth pages
│ ├── login/
│ │ └── +page.svelte
│ └── register/
│ └── +page.svelte
├── (app)/ # Group - no route segment
│ ├── +layout.svelte # Shared layout for app pages
│ ├── dashboard/
│ │ └── +page.svelte
│ └── settings/
│ └── +page.svelte
└── +layout.svelte # Root layout
Breaking Out of Layout
<!-- src/routes/special/+page@.svelte -->
<!-- The @. means use root layout, not parent -->
<script lang="ts">
// This page uses +layout.svelte directly, skipping any nested layouts
</script>
🔀 Navigation
Programmatic Navigation
<script lang="ts">
import { goto, invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
// Navigate to a page
async function navigate() {
await goto('/dashboard');
}
// Navigate with options
async function navigateWithState() {
await goto('/search', {
replaceState: true, // Replace instead of push
keepFocus: true, // Keep focus on current element
noScroll: true, // Don't scroll to top
state: { query: 'test' } // Pass state
});
}
// Invalidate load functions
async function refresh() {
await invalidate('/api/posts'); // Rerun loads that depend on this
// or
await invalidateAll(); // Rerun all load functions
}
</script>
<!-- Current page info -->
<p>Current path: {$page.url.pathname}</p>
<p>Query: {$page.url.searchParams.get('q')}</p>
Link Prefetching
<!-- Preload on hover (default) -->
<a href="/about">About</a>
<!-- Preload on viewport entry -->
<a href="/posts" data-sveltekit-preload-data="hover">Posts</a>
<!-- Disable preloading -->
<a href="/external" data-sveltekit-preload-data="off">External</a>
<!-- Preload immediately when link is rendered -->
<a href="/dashboard" data-sveltekit-preload-data="eager">Dashboard</a>
⚠️ Error Handling
Error Pages
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="error">
<h1>{$page.status}</h1>
<p>{$page.error?.message}</p>
<a href="/">Go home</a>
</div>
<!-- src/routes/blog/+error.svelte -->
<!-- More specific error page for /blog routes -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="blog-error">
<h1>Blog Post Not Found</h1>
<p>{$page.error?.message}</p>
<a href="/blog">View all posts</a>
</div>
Throwing Errors
// In load functions or actions
import { error, redirect } from '@sveltejs/kit';
export const load = async ({ params }) => {
const post = await getPost(params.id);
if (!post) {
error(404, {
message: 'Post not found',
code: 'POST_NOT_FOUND'
});
}
if (post.draft) {
redirect(303, '/blog');
}
return { post };
};
🔧 Hooks
Server Hooks (hooks.server.ts)
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
// Runs for every request
export const handle: Handle = async ({ event, resolve }) => {
// Authentication
const session = event.cookies.get('session');
if (session) {
event.locals.user = await getUserFromSession(session);
}
// Performance timing
const start = Date.now();
const response = await resolve(event, {
// Transform HTML
transformPageChunk: ({ html }) => html.replace('%theme%', 'dark')
});
// Log request duration
console.log(`${event.request.method} ${event.url.pathname} - ${Date.now() - start}ms`);
return response;
};
// Customize server-side fetch
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
// Add auth header to internal API calls
if (request.url.startsWith('https://api.internal.com')) {
request.headers.set('Authorization', `Bearer ${API_KEY}`);
}
return fetch(request);
};
// Handle unexpected errors
export const handleError: HandleServerError = async ({ error, event }) => {
console.error('Server error:', error);
// Report to error tracking service
await reportError(error, event);
return {
message: 'An unexpected error occurred',
code: 'INTERNAL_ERROR'
};
};
Client Hooks (hooks.client.ts)
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({ error, event }) => {
console.error('Client error:', error);
// Report to error tracking
await reportClientError(error);
return {
message: 'Something went wrong',
code: 'CLIENT_ERROR'
};
};
📋 Best Practices
✅ Do:
- Use
+page.server.tsfor sensitive data - Return typed data from load functions
- Use
use:enhancefor progressive enhancement - Handle loading and error states
- Use route groups for layout organization
- Validate form data server-side
❌ Don't:
- Put secrets in
+page.ts(runs on client) - Forget to await
parent()in child loads - Block navigation with slow loads
- Ignore accessibility in forms
- Hardcode URLs (use
$pagestore)