svelte-expert

Deep Svelte 5 and SvelteKit expertise for runes reactivity, component patterns, server-side rendering, form actions, and deployment. Use when building Svelte applications, migrating to Svelte 5 runes, or optimizing SvelteKit projects.

bendourthe/nexus-hub1 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: svelte-expert
description: Deep Svelte 5 and SvelteKit expertise for runes reactivity, component patterns, server-side rendering, form actions, and deployment. Use when building Svelte applications, migrating to Svelte 5 runes, or optimizing SvelteKit projects.
license: MIT
---

# Svelte Expert

Specialized expertise in Svelte 5 and SvelteKit development, providing deep guidance on the runes reactivity system, component composition with snippets, SvelteKit file-based routing, server-side data loading, form actions with progressive enhancement, state management, hooks and middleware, performance optimization, and testing with Vitest and Playwright.

## When to Use This Skill

Use this skill for:

- Building new SvelteKit applications with Svelte 5 runes
- Migrating from Svelte 4 stores to Svelte 5 runes reactivity
- Designing component architectures with snippets, two-way binding, and composition
- Implementing server-side data loading with load functions
- Building form mutations with form actions and progressive enhancement
- Managing shared state with rune-based stores and context API
- Configuring SvelteKit hooks for authentication, error handling, and request modification
- Optimizing performance with fine-grained reactivity, prerendering, and adapter selection
- Writing component tests with @testing-library/svelte and E2E tests with Playwright

**Trigger phrases**: "svelte", "sveltekit", "svelte 5", "runes", "$state", "$derived", "$effect", "$props", "svelte component", "svelte routing", "svelte form actions", "svelte load function", "svelte store", "svelte hooks", "svelte transition", "svelte animation"

## What This Skill Does

Provides Svelte 5 and SvelteKit expertise including:

- **Runes Reactivity**: $state, $derived, $effect, $props, $bindable, $inspect for fine-grained reactivity
- **Component Patterns**: Snippets, event handling, two-way binding, component composition
- **SvelteKit Routing**: File-based routing, layouts, route params, groups, error pages
- **Data Loading**: Load functions, form actions, progressive enhancement, streaming
- **State Management**: Shared rune stores, context API with getContext/setContext, derived state
- **Hooks and Middleware**: handle, handleFetch, handleError, environment variables, service workers
- **Performance**: Fine-grained reactivity, transitions, animations, lazy loading, prerendering, adapters
- **Testing**: Component testing with Vitest and @testing-library/svelte, E2E with Playwright

## Instructions

### Step 1: Master Svelte 5 Runes Fundamentals

Svelte 5 replaces stores and reactive declarations with runes, a set of compiler-level primitives for fine-grained reactivity.

**$state: reactive state declaration**:

```svelte
<script lang="ts">
  let count = $state(0);
  let user = $state<{ name: string; email: string }>({
    name: "Alice",
    email: "alice@example.com",
  });

  // $state creates deeply reactive objects; nested mutations trigger updates
  function updateEmail(email: string) {
    user.email = email; // Triggers reactivity without reassignment
  }
</script>

<button onclick={() => count++}>
  Clicked {count} {count === 1 ? "time" : "times"}
</button>
<p>{user.name} ({user.email})</p>
```

**$state.raw: non-deeply-reactive state for large objects**:

```svelte
<script lang="ts">
  // Use $state.raw when you do not need deep reactivity (large arrays, immutable data)
  let items = $state.raw<string[]>([]);

  function setItems(newItems: string[]) {
    items = newItems; // Must reassign the entire value; mutations are not tracked
  }
</script>
```

**$derived: computed values that update automatically**:

```svelte
<script lang="ts">
  let items = $state<{ name: string; price: number; quantity: number }[]>([
    { name: "Widget", price: 9.99, quantity: 3 },
    { name: "Gadget", price: 24.99, quantity: 1 },
  ]);

  let total = $derived(
    items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  let formattedTotal = $derived(`$${total.toFixed(2)}`);

  // $derived.by for multi-statement computations
  let summary = $derived.by(() => {
    const count = items.length;
    const avgPrice = count > 0 ? total / count : 0;
    return { count, avgPrice, total };
  });
</script>

<p>Cart: {summary.count} items, total {formattedTotal}</p>
```

**$effect: side effects that run when dependencies change**:

```svelte
<script lang="ts">
  let query = $state("");
  let results = $state<string[]>([]);

  // $effect tracks which reactive values are read and reruns when they change
  $effect(() => {
    if (query.length < 2) {
      results = [];
      return;
    }

    const controller = new AbortController();

    fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then((data) => {
        results = data.items;
      })
      .catch((err) => {
        if (err.name !== "AbortError") console.error(err);
      });

    // Return a cleanup function (runs before the effect re-executes)
    return () => controller.abort();
  });
</script>

<input bind:value={query} placeholder="Search..." />
<ul>
  {#each results as result}
    <li>{result}</li>
  {/each}
</ul>
```

**$props and $bindable: component inputs**:

```ts
// UserCard.svelte
<script lang="ts">
  interface Props {
    name: string;
    email: string;
    role?: "admin" | "user";
    onSave?: (data: { name: string; email: string }) => void;
  }

  let { name, email, role = "user", onSave }: Props = $props();
</script>

<div class="card">
  <h3>{name}</h3>
  <p>{email} ({role})</p>
  {#if onSave}
    <button onclick={() => onSave?.({ name, email })}>Save</button>
  {/if}
</div>
```

**$bindable: props that support two-way binding**:

```svelte
<!-- TextInput.svelte -->
<script lang="ts">
  interface Props {
    value: string;
    placeholder?: string;
  }

  let { value = $bindable(""), placeholder = "" }: Props = $props();
</script>

<input bind:value {placeholder} />

<!-- Parent.svelte -->
<script lang="ts">
  import TextInput from "./TextInput.svelte";

  let searchQuery = $state("");
</script>

<TextInput bind:value={searchQuery} placeholder="Search..." />
<p>Current query: {searchQuery}</p>
```

### Step 2: Build Component Patterns

**Snippets: the Svelte 5 replacement for slots**:

```svelte
<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from "svelte";

  interface Props {
    header: Snippet;
    children: Snippet;
    footer?: Snippet;
  }

  let { header, children, footer }: Props = $props();
</script>

<div class="card">
  <div class="card-header">
    {@render header()}
  </div>
  <div class="card-body">
    {@render children()}
  </div>
  {#if footer}
    <div class="card-footer">
      {@render footer()}
    </div>
  {/if}
</div>

<!-- Usage -->
<script lang="ts">
  import Card from "./Card.svelte";
</script>

<Card>
  {#snippet header()}
    <h2>User Profile</h2>
  {/snippet}

  <p>This is the card body content.</p>

  {#snippet footer()}
    <button>Save Changes</button>
  {/snippet}
</Card>
```

**Typed snippets with parameters**:

```svelte
<!-- DataList.svelte -->
<script lang="ts" generics="T">
  import type { Snippet } from "svelte";

  interface Props {
    items: T[];
    renderItem: Snippet<[T, number]>;
    empty?: Snippet;
  }

  let { items, renderItem, empty }: Props = $props();
</script>

{#if items.length === 0}
  {#if empty}
    {@render empty()}
  {:else}
    <p>No items found.</p>
  {/if}
{:else}
  <ul>
    {#each items as item, index}
      <li>{@render renderItem(item, index)}</li>
    {/each}
  </ul>
{/if}

<!-- Usage -->
<script lang="ts">
  import DataList from "./DataList.svelte";

  interface User {
    id: string;
    name: string;
    email: string;
  }

  let users = $state<User[]>([
    { id: "1", name: "Alice", email: "alice@example.com" },
    { id: "2", name: "Bob", email: "bob@example.com" },
  ]);
</script>

<DataList items={users}>
  {#snippet renderItem(user, index)}
    <span>{index + 1}. {user.name} ({user.email})</span>
  {/snippet}
  {#snippet empty()}
    <p>No users registered yet.</p>
  {/snippet}
</DataList>
```

**Event handling and component composition**:

```svelte
<!-- SearchForm.svelte -->
<script lang="ts">
  interface Props {
    onSearch: (query: string) => void;
    initialQuery?: string;
  }

  let { onSearch, initialQuery = "" }: Props = $props();
  let query = $state(initialQuery);
  let inputRef = $state<HTMLInputElement | null>(null);

  function handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    const trimmed = query.trim();
    if (trimmed.length > 0) {
      onSearch(trimmed);
    }
  }

  function handleKeydown(event: KeyboardEvent) {
    if (event.key === "Escape") {
      query = "";
      inputRef?.focus();
    }
  }

  // Focus input on mount
  $effect(() => {
    inputRef?.focus();
  });
</script>

<form onsubmit={handleSubmit}>
  <input
    bind:this={inputRef}
    bind:value={query}
    onkeydown={handleKeydown}
    placeholder="Search..."
    aria-label="Search"
  />
  <button type="submit">Search</button>
</form>
```

**Two-way binding with custom components**:

```svelte
<!-- ColorPicker.svelte -->
<script lang="ts">
  interface Props {
    color: string;
    label?: string;
  }

  let { color = $bindable("#000000"), label = "Color" }: Props = $props();

  let hue = $derived(hexToHue(color));

  function hexToHue(hex: string): number {
    // Simplified hex-to-hue conversion
    const r = parseInt(hex.slice(1, 3), 16) / 255;
    const g = parseInt(hex.slice(3, 5), 16) / 255;
    const b = parseInt(hex.slice(5, 7), 16) / 255;
    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    if (max === min) return 0;
    const d = max - min;
    let h = 0;
    if (max === r) h = ((g - b) / d + 6) % 6;
    else if (max === g) h = (b - r) / d + 2;
    else h = (r - g) / d + 4;
    return Math.round(h * 60);
  }
</script>

<label>
  {label} (hue: {hue})
  <input type="color" bind:value={color} />
</label>
```

### Step 3: Structure SvelteKit Routing

**Standard SvelteKit file structure**:

```
src/
  routes/
    +page.svelte              # Home page (/)
    +layout.svelte            # Root layout
    +layout.server.ts         # Root layout server load
    +error.svelte             # Root error page
    about/
      +page.svelte            # /about
    blog/
      +page.svelte            # /blog (list)
      +page.server.ts         # Blog list data loading
      [slug]/
        +page.svelte          # /blog/:slug (dynamic)
        +page.server.ts       # Single post data loading
    dashboard/
      +layout.svelte          # Dashboard layout (nested)
      +layout.server.ts       # Dashboard auth check
      +page.svelte            # /dashboard
      settings/
        +page.svelte          # /dashboard/settings
    api/
      health/
        +server.ts            # GET /api/health
      users/
        +server.ts            # GET/POST /api/users
        [id]/
          +server.ts          # GET/PUT/DELETE /api/users/:id
    (marketing)/              # Route group (no URL segment)
      pricing/
        +page.svelte          # /pricing
      contact/
        +page.svelte          # /contact
  lib/
    server/                   # Server-only modules ($lib/server/)
      db.ts
      auth.ts
    components/               # Shared components ($lib/components/)
    stores/                   # Shared state ($lib/stores/)
```

**Root layout with navigation**:

```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import type { LayoutData } from "./$types";
  import { page } from "$app/stores";

  let { data, children }: { data: LayoutData; children: any } = $props();
</script>

<div class="app">
  <header>
    <nav aria-label="Main navigation">
      <a href="/" class:active={$page.url.pathname === "/"}>Home</a>
      <a href="/blog" class:active={$page.url.pathname.startsWith("/blog")}>Blog</a>
      <a href="/dashboard" class:active={$page.url.pathname.startsWith("/dashboard")}>
        Dashboard
      </a>
      {#if data.user}
        <span>Welcome, {data.user.name}</span>
      {:else}
        <a href="/login">Sign In</a>
      {/if}
    </nav>
  </header>
  <main>
    {@render children()}
  </main>
  <footer>
    <p>Built with SvelteKit</p>
  </footer>
</div>

<style>
  .active {
    font-weight: bold;
    text-decoration: underline;
  }
</style>
```

**Dynamic route with params and error handling**:

```svelte
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";

  let { data }: { data: PageData } = $props();
</script>

<svelte:head>
  <title>{data.post.title} | My Blog</title>
  <meta name="description" content={data.post.excerpt} />
</svelte:head>

<article>
  <h1>{data.post.title}</h1>
  <time datetime={data.post.publishedAt}>
    {new Date(data.post.publishedAt).toLocaleDateString()}
  </time>
  <div class="content">
    {@html data.post.htmlContent}
  </div>
</article>
```

**Route groups and layout resets**:

```svelte
<!-- src/routes/(marketing)/+layout.svelte -->
<script lang="ts">
  // Marketing pages get a different layout than the dashboard
  let { children }: { children: any } = $props();
</script>

<div class="marketing-layout">
  <header class="marketing-header">
    <a href="/">Brand Logo</a>
    <a href="/pricing">Pricing</a>
    <a href="/contact">Contact</a>
  </header>
  {@render children()}
</div>
```

**API route handlers (+server.ts)**:

```ts
// src/routes/api/users/+server.ts
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { db } from "$lib/server/db";

export const GET: RequestHandler = async ({ url }) => {
  const page = parseInt(url.searchParams.get("page") ?? "1", 10);
  const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return json({ data: users, page, limit });
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();

  if (!body.email || !body.name) {
    error(400, { message: "Name and email are required" });
  }

  const user = await db.user.create({ data: body });
  return json(user, { status: 201 });
};
```

### Step 4: Implement Data Loading and Form Actions

**Server load functions (+page.server.ts)**:

```ts
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from "./$types";
import { db } from "$lib/server/db";

export const load: PageServerLoad = async ({ url, depends }) => {
  const page = parseInt(url.searchParams.get("page") ?? "1", 10);
  const limit = 10;

  // depends() registers a custom invalidation key
  depends("app:posts");

  const [posts, total] = await Promise.all([
    db.post.findMany({
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { publishedAt: "desc" },
      select: { slug: true, title: true, excerpt: true, publishedAt: true },
    }),
    db.post.count(),
  ]);

  return {
    posts,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
};
```

**Dynamic route load with error handling**:

```ts
// src/routes/blog/[slug]/+page.server.ts
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { db } from "$lib/server/db";

export const load: PageServerLoad = async ({ params }) => {
  const post = await db.post.findUnique({
    where: { slug: params.slug },
  });

  if (!post) {
    error(404, { message: `Post "${params.slug}" not found` });
  }

  return { post };
};
```

**Form actions with progressive enhancement**:

```ts
// src/routes/blog/new/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import { db } from "$lib/server/db";
import { z } from "zod";

const CreatePostSchema = z.object({
  title: z.string().min(1, "Title is required").max(200),
  content: z.string().min(10, "Content must be at least 10 characters"),
  published: z.boolean().default(false),
});

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) {
    redirect(302, "/login");
  }
  return {};
};

export const actions = {
  default: async ({ request, locals }) => {
    const formData = await request.formData();

    const parsed = CreatePostSchema.safeParse({
      title: formData.get("title"),
      content: formData.get("content"),
      published: formData.has("published"),
    });

    if (!parsed.success) {
      return fail(400, {
        errors: parsed.error.flatten().fieldErrors,
        values: {
          title: formData.get("title") as string,
          content: formData.get("content") as string,
        },
      });
    }

    const post = await db.post.create({
      data: {
        ...parsed.data,
        slug: parsed.data.title.toLowerCase().replace(/\s+/g, "-"),
        authorId: locals.user!.id,
      },
    });

    redirect(303, `/blog/${post.slug}`);
  },
} satisfies Actions;
```

**Form component with progressive enhancement**:

```svelte
<!-- src/routes/blog/new/+page.svelte -->
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { ActionData } from "./$types";

  let { form }: { form: ActionData } = $props();
  let submitting = $state(false);
</script>

<h1>New Blog Post</h1>

<form
  method="POST"
  use:enhance={() => {
    submitting = true;
    return async ({ update }) => {
      submitting = false;
      await update();
    };
  }}
>
  <label for="title">Title</label>
  <input
    id="title"
    name="title"
    value={form?.values?.title ?? ""}
    required
    aria-invalid={form?.errors?.title ? "true" : undefined}
    aria-describedby={form?.errors?.title ? "title-error" : undefined}
  />
  {#if form?.errors?.title}
    <p id="title-error" class="error">{form.errors.title[0]}</p>
  {/if}

  <label for="content">Content</label>
  <textarea
    id="content"
    name="content"
    required
    aria-invalid={form?.errors?.content ? "true" : undefined}
    aria-describedby={form?.errors?.content ? "content-error" : undefined}
  >{form?.values?.content ?? ""}</textarea>
  {#if form?.errors?.content}
    <p id="content-error" class="error">{form.errors.content[0]}</p>
  {/if}

  <label>
    <input type="checkbox" name="published" />
    Publish immediately
  </label>

  <button type="submit" disabled={submitting}>
    {submitting ? "Creating..." : "Create Post"}
  </button>
</form>
```

**Streaming with promises in load functions**:

```ts
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types";
import { db } from "$lib/server/db";

export const load: PageServerLoad = async ({ locals }) => {
  // Return fast data immediately, stream slow data
  const quickStats = await db.stats.getQuick(locals.user!.id);

  return {
    stats: quickStats,
    // These promises stream to the client as they resolve
    recentActivity: db.activity.findRecent(locals.user!.id),
    recommendations: db.recommendations.generate(locals.user!.id),
  };
};
```

```svelte
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";

  let { data }: { data: PageData } = $props();
</script>

<h1>Dashboard</h1>

<!-- Immediately available -->
<div class="stats">
  <p>Total posts: {data.stats.postCount}</p>
  <p>Total views: {data.stats.viewCount}</p>
</div>

<!-- Streamed in when ready -->
{#await data.recentActivity}
  <div class="skeleton">Loading recent activity...</div>
{:then activity}
  <ul>
    {#each activity as item}
      <li>{item.description} - {item.timestamp}</li>
    {/each}
  </ul>
{:catch error}
  <p class="error">Failed to load activity: {error.message}</p>
{/await}

{#await data.recommendations}
  <div class="skeleton">Generating recommendations...</div>
{:then recs}
  <ul>
    {#each recs as rec}
      <li>{rec.title}</li>
    {/each}
  </ul>
{:catch}
  <p class="error">Could not generate recommendations.</p>
{/await}
```

### Step 5: Manage State

**Shared rune-based stores**:

```ts
// src/lib/stores/cart.svelte.ts
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

function createCartStore() {
  let items = $state<CartItem[]>([]);

  let total = $derived(
    items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  let count = $derived(
    items.reduce((sum, item) => sum + item.quantity, 0)
  );

  function addItem(product: Omit<CartItem, "quantity">) {
    const existing = items.find((item) => item.id === product.id);
    if (existing) {
      existing.quantity += 1;
    } else {
      items.push({ ...product, quantity: 1 });
    }
  }

  function removeItem(id: string) {
    const index = items.findIndex((item) => item.id === id);
    if (index !== -1) {
      items.splice(index, 1);
    }
  }

  function updateQuantity(id: string, quantity: number) {
    const item = items.find((i) => i.id === id);
    if (item) {
      item.quantity = Math.max(0, quantity);
      if (item.quantity === 0) removeItem(id);
    }
  }

  function clear() {
    items.length = 0;
  }

  return {
    get items() { return items; },
    get total() { return total; },
    get count() { return count; },
    addItem,
    removeItem,
    updateQuantity,
    clear,
  };
}

export const cart = createCartStore();
```

```svelte
<!-- CartWidget.svelte -->
<script lang="ts">
  import { cart } from "$lib/stores/cart.svelte";
</script>

<div class="cart-widget">
  <span>Cart ({cart.count} items): ${cart.total.toFixed(2)}</span>
  {#each cart.items as item}
    <div class="cart-item">
      <span>{item.name} x {item.quantity}</span>
      <button onclick={() => cart.updateQuantity(item.id, item.quantity - 1)}>-</button>
      <button onclick={() => cart.updateQuantity(item.id, item.quantity + 1)}>+</button>
      <button onclick={() => cart.removeItem(item.id)}>Remove</button>
    </div>
  {/each}
  {#if cart.count > 0}
    <button onclick={() => cart.clear()}>Clear Cart</button>
  {/if}
</div>
```

**Context API with getContext/setContext**:

```svelte
<!-- ThemeProvider.svelte -->
<script lang="ts">
  import { setContext } from "svelte";

  interface ThemeContext {
    theme: string;
    toggleTheme: () => void;
  }

  let theme = $state<"light" | "dark">("light");

  function toggleTheme() {
    theme = theme === "light" ? "dark" : "light";
  }

  // Context is available to all descendants
  setContext<ThemeContext>("theme", {
    get theme() { return theme; },
    toggleTheme,
  });

  let { children }: { children: any } = $props();
</script>

<div class="app" data-theme={theme}>
  {@render children()}
</div>

<!-- ThemeToggle.svelte (descendant component) -->
<script lang="ts">
  import { getContext } from "svelte";

  interface ThemeContext {
    theme: string;
    toggleTheme: () => void;
  }

  const { theme, toggleTheme } = getContext<ThemeContext>("theme");
</script>

<button onclick={toggleTheme}>
  Current theme: {theme}. Click to toggle.
</button>
```

**Derived state across multiple stores**:

```ts
// src/lib/stores/filters.svelte.ts
interface FilterState {
  category: string;
  minPrice: number;
  maxPrice: number;
  searchQuery: string;
}

function createFilterStore() {
  let filters = $state<FilterState>({
    category: "all",
    minPrice: 0,
    maxPrice: Infinity,
    searchQuery: "",
  });

  let activeFilterCount = $derived(
    [
      filters.category !== "all",
      filters.minPrice > 0,
      filters.maxPrice < Infinity,
      filters.searchQuery.length > 0,
    ].filter(Boolean).length
  );

  let queryParams = $derived.by(() => {
    const params = new URLSearchParams();
    if (filters.category !== "all") params.set("category", filters.category);
    if (filters.minPrice > 0) params.set("minPrice", String(filters.minPrice));
    if (filters.maxPrice < Infinity) params.set("maxPrice", String(filters.maxPrice));
    if (filters.searchQuery) params.set("q", filters.searchQuery);
    return params.toString();
  });

  function setCategory(category: string) {
    filters.category = category;
  }

  function setPriceRange(min: number, max: number) {
    filters.minPrice = min;
    filters.maxPrice = max;
  }

  function setSearch(query: string) {
    filters.searchQuery = query;
  }

  function reset() {
    filters.category = "all";
    filters.minPrice = 0;
    filters.maxPrice = Infinity;
    filters.searchQuery = "";
  }

  return {
    get filters() { return filters; },
    get activeFilterCount() { return activeFilterCount; },
    get queryParams() { return queryParams; },
    setCategory,
    setPriceRange,
    setSearch,
    reset,
  };
}

export const filterStore = createFilterStore();
```

### Step 6: Configure SvelteKit Hooks and Middleware

**Server hooks (src/hooks.server.ts)**:

```ts
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from "@sveltejs/kit";
import { db } from "$lib/server/db";
import { verifySession } from "$lib/server/auth";
import { sequence } from "@sveltejs/kit/hooks";

// Authentication hook
const authHandle: Handle = async ({ event, resolve }) => {
  const sessionToken = event.cookies.get("session");

  if (sessionToken) {
    try {
      const user = await verifySession(sessionToken);
      event.locals.user = user;
    } catch {
      // Invalid session; clear the cookie
      event.cookies.delete("session", { path: "/" });
    }
  }

  return resolve(event);
};

// Security headers hook
const securityHandle: Handle = async ({ event, resolve }) => {
  const response = await resolve(event);

  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=()"
  );

  return response;
};

// Protected routes hook
const protectedRoutes: Handle = async ({ event, resolve }) => {
  const protectedPaths = ["/dashboard", "/settings", "/admin"];
  const isProtected = protectedPaths.some((p) =>
    event.url.pathname.startsWith(p)
  );

  if (isProtected && !event.locals.user) {
    const redirectUrl = `/login?redirect=${encodeURIComponent(event.url.pathname)}`;
    return new Response(null, {
      status: 302,
      headers: { location: redirectUrl },
    });
  }

  if (event.url.pathname.startsWith("/admin")) {
    if (event.locals.user?.role !== "admin") {
      return new Response("Forbidden", { status: 403 });
    }
  }

  return resolve(event);
};

// Chain hooks with sequence
export const handle = sequence(authHandle, securityHandle, protectedRoutes);

// Modify outgoing fetch requests (e.g., attach auth headers to API calls)
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
  if (request.url.startsWith("https://api.internal.example.com")) {
    request.headers.set(
      "Authorization",
      `Bearer ${event.locals.user?.apiToken ?? ""}`
    );
  }
  return fetch(request);
};

// Global error handler
export const handleError: HandleServerError = async ({ error, event, status, message }) => {
  const errorId = crypto.randomUUID();

  console.error(`[${errorId}] Unhandled error on ${event.url.pathname}:`, error);

  // Report to error tracking service
  // await reportError({ errorId, error, url: event.url.pathname });

  return {
    message: "An unexpected error occurred",
    errorId,
  };
};
```

**App.d.ts type declarations**:

```ts
// src/app.d.ts
declare global {
  namespace App {
    interface Error {
      message: string;
      errorId?: string;
    }
    interface Locals {
      user: {
        id: string;
        name: string;
        email: string;
        role: "admin" | "user";
        apiToken?: string;
      } | null;
    }
    interface PageData {
      user: App.Locals["user"];
    }
    interface PageState {}
    interface Platform {}
  }
}

export {};
```

**Environment variables**:

```ts
// Access public env variables (available on client and server)
import { PUBLIC_API_URL, PUBLIC_APP_NAME } from "$env/static/public";

// Access private env variables (server-only, build-time)
import { DATABASE_URL, JWT_SECRET } from "$env/static/private";

// Dynamic env variables (read at runtime, not inlined at build)
import { env } from "$env/dynamic/private";
const dbUrl = env.DATABASE_URL;

// SvelteKit will throw a build error if you try to import
// private env variables into client-side code
```

**Custom error page**:

```svelte
<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from "$app/stores";
</script>

<svelte:head>
  <title>Error {$page.status}</title>
</svelte:head>

<div class="error-page">
  <h1>{$page.status}</h1>
  <p>{$page.error?.message ?? "Something went wrong"}</p>
  {#if $page.error?.errorId}
    <p class="error-id">Error ID: {$page.error.errorId}</p>
  {/if}
  <a href="/">Go home</a>
</div>
```

### Step 7: Optimize Performance

**Fine-grained reactivity with runes**:

```svelte
<script lang="ts">
  // Svelte 5 runes provide fine-grained reactivity at the signal level.
  // Only the specific DOM nodes that depend on a changed value will update.

  let firstName = $state("Alice");
  let lastName = $state("Smith");

  // Only elements reading fullName re-render when firstName or lastName changes
  let fullName = $derived(`${firstName} ${lastName}`);

  // Avoid $effect for derived state; use $derived instead
  // WRONG: $effect(() => { fullName = `${firstName} ${lastName}` });
  // RIGHT: let fullName = $derived(`${firstName} ${lastName}`);
</script>

<input bind:value={firstName} /> <!-- Updating this does NOT re-render the lastName input -->
<input bind:value={lastName} />
<p>Hello, {fullName}</p>
```

**Transitions and animations**:

```svelte
<script lang="ts">
  import { fade, fly, slide, scale } from "svelte/transition";
  import { flip } from "svelte/animate";
  import { quintOut } from "svelte/easing";

  let items = $state<{ id: string; text: string }[]>([]);
  let showPanel = $state(false);

  function addItem() {
    items.push({ id: crypto.randomUUID(), text: `Item ${items.length + 1}` });
  }

  function removeItem(id: string) {
    const index = items.findIndex((i) => i.id === id);
    if (index !== -1) items.splice(index, 1);
  }
</script>

<button onclick={() => (showPanel = !showPanel)}>Toggle Panel</button>

{#if showPanel}
  <div transition:slide={{ duration: 300, easing: quintOut }}>
    <p>This panel slides in and out.</p>
  </div>
{/if}

<button onclick={addItem}>Add Item</button>

<ul>
  {#each items as item (item.id)}
    <li
      animate:flip={{ duration: 200 }}
      in:fly={{ y: 20, duration: 200 }}
      out:fade={{ duration: 150 }}
    >
      {item.text}
      <button onclick={() => removeItem(item.id)}>Remove</button>
    </li>
  {/each}
</ul>
```

**Lazy loading with dynamic imports**:

```svelte
<script lang="ts">
  let showChart = $state(false);
  let ChartComponent: any = $state(null);

  async function loadChart() {
    showChart = true;
    // Dynamic import splits the chart library into a separate chunk
    const module = await import("$lib/components/HeavyChart.svelte");
    ChartComponent = module.default;
  }
</script>

<button onclick={loadChart}>Show Analytics</button>

{#if showChart}
  {#if ChartComponent}
    <ChartComponent data={chartData} />
  {:else}
    <p>Loading chart...</p>
  {/if}
{/if}
```

**Prerendering and adapter selection**:

```ts
// svelte.config.js
import adapter from "@sveltejs/adapter-auto"; // Auto-detects Vercel, Netlify, Cloudflare
// import adapter from "@sveltejs/adapter-node";     // Self-hosted Node.js
// import adapter from "@sveltejs/adapter-static";   // Fully static site
// import adapter from "@sveltejs/adapter-vercel";   // Vercel-specific features

import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    prerender: {
      // Prerender these routes at build time
      entries: ["/", "/about", "/pricing", "/blog"],
    },
    csp: {
      directives: {
        "script-src": ["self"],
        "style-src": ["self", "unsafe-inline"],
      },
    },
    alias: {
      $components: "src/lib/components",
      $stores: "src/lib/stores",
    },
  },
};

export default config;
```

**Per-page prerendering and SSR control**:

```ts
// src/routes/about/+page.ts
// Static page: prerender at build time
export const prerender = true;

// src/routes/dashboard/+page.ts
// Dynamic page: never prerender, always SSR
export const prerender = false;
export const ssr = true;

// src/routes/app/+page.ts
// SPA page: disable SSR, render client-only
export const ssr = false;
export const csr = true;
```

### Step 8: Test with Vitest and Playwright

**Component testing with @testing-library/svelte**:

```ts
// src/lib/components/Counter.test.ts
import { render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import Counter from "./Counter.svelte";

describe("Counter", () => {
  it("renders initial count", () => {
    render(Counter, { props: { initial: 5 } });
    expect(screen.getByText("Count: 5")).toBeInTheDocument();
  });

  it("increments count on button click", async () => {
    const user = userEvent.setup();
    render(Counter, { props: { initial: 0 } });

    const button = screen.getByRole("button", { name: /increment/i });
    await user.click(button);

    expect(screen.getByText("Count: 1")).toBeInTheDocument();
  });

  it("decrements count but not below zero", async () => {
    const user = userEvent.setup();
    render(Counter, { props: { initial: 0 } });

    const button = screen.getByRole("button", { name: /decrement/i });
    await user.click(button);

    expect(screen.getByText("Count: 0")).toBeInTheDocument();
  });

  it("calls onChange callback when count changes", async () => {
    const user = userEvent.setup();
    const onChange = vi.fn();
    render(Counter, { props: { initial: 0, onChange } });

    await user.click(screen.getByRole("button", { name: /increment/i }));

    expect(onChange).toHaveBeenCalledWith(1);
  });
});
```

**Testing components with context and stores**:

```ts
// src/lib/components/CartSummary.test.ts
import { render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, beforeEach } from "vitest";
import CartSummary from "./CartSummary.svelte";
import { cart } from "$lib/stores/cart.svelte";

describe("CartSummary", () => {
  beforeEach(() => {
    cart.clear();
  });

  it("shows empty cart message when no items", () => {
    render(CartSummary);
    expect(screen.getByText(/cart is empty/i)).toBeInTheDocument();
  });

  it("displays item count and total after adding items", async () => {
    cart.addItem({ id: "1", name: "Widget", price: 9.99 });
    cart.addItem({ id: "2", name: "Gadget", price: 24.99 });

    render(CartSummary);

    expect(screen.getByText(/2 items/i)).toBeInTheDocument();
    expect(screen.getByText(/\$34\.98/)).toBeInTheDocument();
  });

  it("removes item when remove button is clicked", async () => {
    const user = userEvent.setup();
    cart.addItem({ id: "1", name: "Widget", price: 9.99 });

    render(CartSummary);
    await user.click(screen.getByRole("button", { name: /remove widget/i }));

    expect(screen.getByText(/cart is empty/i)).toBeInTheDocument();
  });
});
```

**Testing load functions in isolation**:

```ts
// src/routes/blog/[slug]/+page.server.test.ts
import { describe, it, expect, vi } from "vitest";
import { load } from "./+page.server";

vi.mock("$lib/server/db", () => ({
  db: {
    post: {
      findUnique: vi.fn(),
    },
  },
}));

import { db } from "$lib/server/db";

describe("blog post load function", () => {
  it("returns post data for a valid slug", async () => {
    const mockPost = {
      slug: "hello-world",
      title: "Hello World",
      htmlContent: "<p>Content</p>",
      publishedAt: "2026-01-15",
      excerpt: "A hello world post",
    };

    vi.mocked(db.post.findUnique).mockResolvedValue(mockPost);

    const result = await load({
      params: { slug: "hello-world" },
    } as any);

    expect(result.post).toEqual(mockPost);
    expect(db.post.findUnique).toHaveBeenCalledWith({
      where: { slug: "hello-world" },
    });
  });

  it("throws 404 error for a missing slug", async () => {
    vi.mocked(db.post.findUnique).mockResolvedValue(null);

    await expect(
      load({ params: { slug: "nonexistent" } } as any)
    ).rejects.toMatchObject({
      status: 404,
    });
  });
});
```

**End-to-end testing with Playwright**:

```ts
// tests/e2e/blog.test.ts
import { test, expect } from "@playwright/test";

test.describe("Blog", () => {
  test("displays list of blog posts", async ({ page }) => {
    await page.goto("/blog");

    await expect(page.getByRole("heading", { name: /blog/i })).toBeVisible();
    await expect(page.getByRole("article").first()).toBeVisible();
  });

  test("navigates to individual post and back", async ({ page }) => {
    await page.goto("/blog");

    const firstPost = page.getByRole("article").first();
    const postTitle = await firstPost.getByRole("heading").textContent();
    await firstPost.getByRole("link").click();

    await expect(page.getByRole("heading", { name: postTitle! })).toBeVisible();

    await page.goBack();
    await expect(page.getByRole("heading", { name: /blog/i })).toBeVisible();
  });

  test("creates a new blog post via form", async ({ page }) => {
    // Log in first
    await page.goto("/login");
    await page.getByLabel(/email/i).fill("admin@example.com");
    await page.getByLabel(/password/i).fill("testpassword");
    await page.getByRole("button", { name: /sign in/i }).click();

    await page.goto("/blog/new");
    await page.getByLabel(/title/i).fill("E2E Test Post");
    await page.getByLabel(/content/i).fill("This post was created by an E2E test to verify form submission.");
    await page.getByRole("button", { name: /create post/i }).click();

    // Should redirect to the new post
    await expect(page.getByRole("heading", { name: "E2E Test Post" })).toBeVisible();
  });

  test("shows validation errors for empty form", async ({ page }) => {
    await page.goto("/login");
    await page.getByLabel(/email/i).fill("admin@example.com");
    await page.getByLabel(/password/i).fill("testpassword");
    await page.getByRole("button", { name: /sign in/i }).click();

    await page.goto("/blog/new");
    await page.getByRole("button", { name: /create post/i }).click();

    await expect(page.getByText(/title is required/i)).toBeVisible();
  });
});

test.describe("Dashboard", () => {
  test("redirects unauthenticated users to login", async ({ page }) => {
    await page.goto("/dashboard");
    await expect(page).toHaveURL(/\/login/);
  });

  test("displays dashboard stats for authenticated users", async ({ page }) => {
    await page.goto("/login");
    await page.getByLabel(/email/i).fill("admin@example.com");
    await page.getByLabel(/password/i).fill("testpassword");
    await page.getByRole("button", { name: /sign in/i }).click();

    await page.goto("/dashboard");
    await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible();
    await expect(page.getByText(/total posts/i)).toBeVisible();
  });
});
```

**Vitest configuration for SvelteKit**:

```ts
// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    include: ["src/**/*.{test,spec}.{js,ts}"],
    globals: true,
    environment: "jsdom",
    setupFiles: ["./src/tests/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "html", "lcov"],
      include: ["src/lib/**/*.{ts,svelte}", "src/routes/**/*.{ts,svelte}"],
      thresholds: {
        lines: 80,
        branches: 70,
      },
    },
  },
});
```

```ts
// src/tests/setup.ts
import "@testing-library/jest-dom/vitest";
```

**Playwright configuration**:

```ts
// playwright.config.ts
import type { PlaywrightTestConfig } from "@playwright/test";

const config: PlaywrightTestConfig = {
  webServer: {
    command: "npm run build && npm run preview",
    port: 4173,
    reuseExistingServer: !process.env.CI,
  },
  testDir: "tests/e2e",
  testMatch: /(.+\.)?(test|spec)\.[jt]s/,
  use: {
    baseURL: "http://localhost:4173",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI ? "github" : "list",
};

export default config;
```

## Best Practices

- **Use runes, not stores**: Svelte 5 runes ($state, $derived, $effect) replace writable/readable/derived stores with simpler, more performant primitives
- **Prefer $derived over $effect for computed values**: $effect is for side effects (fetches, DOM manipulation, logging); $derived is for transformations
- **Use progressive enhancement**: SvelteKit forms work without JavaScript by default; add `use:enhance` for a smoother UX, not as a requirement
- **Colocate data loading with routes**: each +page.server.ts loads only the data its +page.svelte needs
- **Stream slow data**: return promises from load functions and use `{#await}` blocks so the page renders immediately
- **Choose the right adapter**: adapter-auto for most deployments; adapter-node for self-hosted; adapter-static for fully prerendered sites
- **Keep components small**: extract logic into .svelte.ts modules and rendering into child components
- **Type your props with interfaces**: use the Props interface pattern with $props() for full TypeScript safety

## Common Patterns

### Pattern 1: Debounced Search with Runes

```svelte
<script lang="ts">
  let query = $state("");
  let results = $state<string[]>([]);
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;

  $effect(() => {
    clearTimeout(debounceTimer);

    if (query.length < 2) {
      results = [];
      return;
    }

    debounceTimer = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      if (res.ok) {
        results = await res.json();
      }
    }, 300);

    return () => clearTimeout(debounceTimer);
  });
</script>

<input bind:value={query} placeholder="Search..." />
{#each results as result}
  <p>{result}</p>
{/each}
```

### Pattern 2: Authenticated Layout with Redirect

```ts
// src/routes/dashboard/+layout.server.ts
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";

export const load: LayoutServerLoad = async ({ locals }) => {
  if (!locals.user) {
    redirect(302, "/login");
  }

  return {
    user: locals.user,
  };
};
```

```svelte
<!-- src/routes/dashboard/+layout.svelte -->
<script lang="ts">
  import type { LayoutData } from "./$types";

  let { data, children }: { data: LayoutData; children: any } = $props();
</script>

<div class="dashboard-layout">
  <aside>
    <nav>
      <p>Signed in as {data.user.name}</p>
      <a href="/dashboard">Overview</a>
      <a href="/dashboard/settings">Settings</a>
    </nav>
  </aside>
  <div class="dashboard-content">
    {@render children()}
  </div>
</div>
```

## Common Rationalizations

| Rationalization | Reality |
|---|---|
| "I will compute the derived value inside an $effect that assigns to $state." | Using $effect to derive state creates an extra render pass and an easy infinite-loop trap when the effect's write feeds its own dependency. $derived computes synchronously without re-running the component; that is what it is for. |
| "Svelte 4 stores still work, so I will keep using them in this Svelte 5 project." | Mixing store reactivity with runes fragments the reactivity model and forfeits the fine-grained updates Svelte 5 provides. Worse, the two paradigms interact subtly and produce updates that fire in the wrong order. Migrate to $state and shared runes. |
| "The form posts to a server action, so I do not need to re-validate the input there." | Form actions are public endpoints reachable without `use:enhance`; a direct POST bypasses any client-side checks entirely. Validate with Zod and return `fail()` in the action, or unvalidated data hits your mutation. |

## Verification

- [ ] All reactive state uses $state (not Svelte 4 stores or let-assignment reactivity)
- [ ] Computed values use $derived, not $effect with assignment
- [ ] $effect cleanup functions handle abort controllers and timers
- [ ] Components use $props() with typed interfaces
- [ ] Snippets replace all slot usage (Svelte 5 pattern)
- [ ] Load functions validate params and return typed data
- [ ] Form actions validate input with Zod and return fail() on error
- [ ] Forms use use:enhance for progressive enhancement
- [ ] hooks.server.ts applies auth checks and security headers
- [ ] Private env variables are never imported in client code
- [ ] Transitions use easing functions for smooth motion
- [ ] Prerender is configured for static pages
- [ ] Component tests use @testing-library/svelte with accessible queries
- [ ] E2E tests cover critical user flows (auth, form submission, navigation)
- [ ] Vitest coverage meets 80% line threshold

## Related Skills

- [[react-expert]] - React component architecture and hooks (for comparison)
- [[nextjs-expert]] - Next.js App Router patterns (SvelteKit equivalent concepts)
- [[typescript-expert]] - Advanced TypeScript for typed components and stores
- [[unit-tests]] - General unit testing strategies
- [[e2e-testing-automation]] - End-to-end testing patterns with Playwright

---

**Version**: 1.0.0
**Last Updated**: March 2026

### Iterative Refinement Strategy
This skill is optimized for an iterative approach:
1. **Execute**: Perform the core steps defined above.
2. **Review**: Critically analyze the output (coverage, quality, completeness).
3. **Refine**: If targets aren't met, repeat the specific implementation steps with improved context.
4. **Loop**: Continue until the definition of done is satisfied.

More Deployment & CI/CD skills

finetuning

microsoft/azure-skills

Fine-tune models on Microsoft Foundry using SFT (supervised), DPO (preference), or RFT (reinforcement with graders). Covers dataset preparation, training job submission, deployment, and evaluation. USE FOR: fine-tune, SFT, DPO, RFT, training data, grader, distillation, fine-tuned model, training job, large file upload, calibrate grader, deploy fine-tuned model, evaluate fine-tuned model. DO NOT USE FOR: general model deployment without fine-tuning (use deploy-model), agent creation (use agents), prompt optimization without training (use prompt-optimizer).

323.2k

prisma-compute

prisma/skills

Prisma Compute deployment and hosting guide. Use whenever the user mentions Prisma Compute, `prisma.compute.ts`, `defineComputeConfig`, deploying or hosting a Prisma app, `@prisma/cli app deploy`, `compute:deploy`, `create-prisma --deploy`, `PRISMA_SERVICE_TOKEN`, Compute auth/workspaces, apps/deployments/build logs/domains, localhost vs `0.0.0.0`, deploy port binding, or framework deploy readiness for Hono, Elysia, Next.js, TanStack Start, Astro, Nuxt, Svelte, Nest, Turborepo, or custom/prebuilt artifacts.

231.4k

azure-quotas

microsoft/azure-skills

Check/manage Azure quotas and usage across providers. For deployment planning, capacity validation, region selection. WHEN: \"check quotas\", \"service limits\", \"current usage\", \"request quota increase\", \"quota exceeded\", \"validate capacity\", \"regional availability\", \"provisioning limits\", \"vCPU limit\", \"how many vCPUs available in my subscription\".

187.6k

← All Deployment & CI/CD skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY