Files
Job-Info/svelte-reference/04-SVELTEKIT_ROUTING.md
T
aewing 0b76a4b119 Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- 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
2026-01-21 03:56:01 +00:00

729 lines
16 KiB
Markdown

# 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)
```typescript
// 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
};
};
```
```svelte
<!-- 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)
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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');
}
};
```
```svelte
<!-- 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
```typescript
// 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, '/');
}
};
```
```svelte
<!-- 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
```typescript
// 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
```svelte
<!-- 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
```svelte
<!-- 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
```svelte
<!-- 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
```svelte
<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
```svelte
<!-- 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
```svelte
<!-- 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
```typescript
// 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)
```typescript
// 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)
```typescript
// 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.ts` for sensitive data
- Return typed data from load functions
- Use `use:enhance` for 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 `$page` store)