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

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>
<!-- 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.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)