SvelteKit
Purpose
Define and enforce SvelteKit application architecture with routing conventions, data loading, form handling, and deployment patterns.
Agent Protocol
Trigger
User request includes: svelte, sveltekit, svelte app, svelte routing, svelte load, svelte server, svelte form, svelte kit.
Input Context
- SvelteKit version (2.x)
- Rendering mode (SSR, SPA, static)
- Data fetching requirements
- Auth strategy
Output Artifact
No file output. Produces route structure, load functions, form actions, and deployment config as text.
Response Format
Route structure with file tree. Load and action code examples. Deployment config.
No preamble. No postamble. No explanations. No filler/hedging/transitions.
Max Response Length
4096 tokens
Component Architecture / Decision Trees
Architecture Options
| Approach | Trade-off | When to Use |
|---|---|---|
| Server load functions (+page.server.ts) | DB access, secrets, SSR | Private/personalized data |
| Universal load functions (+page.ts) | Runs on server + client | Public API data, cached |
| Form actions | Server mutations, progressive enhancement | All form submissions |
| API endpoints (+server.ts) | REST/JSON endpoints | External API consumption |
| Hooks (handle) | Per-request processing | Auth, logging, redirects |
Decision Tree: Load Function
Is the data user-specific?
├── Yes -> +page.server.ts or +layout.server.ts
└── No -> Is the API public?
├── Yes -> +page.ts (universal load)
└── No -> +page.server.ts
Decision Tree: SSR vs SPA vs Static
How should this route render?
├── Needs SEO + fast initial load -> SSR (default)
├── Fully static content -> export const prerender = true
├── Authenticated dashboard -> SSR + trailing slash
└── No SSR needed -> export const ssr = false
Component Design Patterns
Server Load with Auth
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ locals, url }) => {
if (!locals.user) throw redirect(302, '/login')
const page = Number(url.searchParams.get('page')) || 1
const [orders, total, notifications] = await Promise.all([
db.order.findMany({ where: { userId: locals.user.id }, skip: (page-1)*20, take: 20 }),
db.order.count({ where: { userId: locals.user.id } }),
db.notification.findMany({ where: { userId: locals.user.id, read: false } }),
])
return { user: locals.user, orders, total, page, notifications }
}
Universal Load with Caching
// src/routes/products/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch, url }) => {
const res = await fetch(`/api/products?${url.searchParams}`)
const products = await res.json()
return {
products,
/** Cache on CDN for 5 minutes, stale-while-revalidate for 1 hour */
headers: { 'Cache-Control': 'public, max-age=300, s-maxage=3600' },
}
}
Form Action with Validation
// src/routes/settings/+page.server.ts
import type { Actions } from './$types'
import { fail, redirect } from '@sveltejs/kit'
import { z } from 'zod'
const schema = z.object({ name: z.string().min(2), email: z.string().email() })
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = Object.fromEntries(await request.formData())
const result = schema.safeParse(data)
if (!result.success) return fail(400, { errors: result.error.flatten().fieldErrors, values: data })
await db.user.update({ where: { id: locals.user.id }, data: result.data })
return { success: true }
},
delete: async ({ locals }) => {
await db.user.delete({ where: { id: locals.user.id } })
throw redirect(302, '/goodbye')
},
}
API Endpoint
// src/routes/api/orders/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ locals, url }) => {
const orders = await db.order.findMany({ where: { userId: locals.user.id } })
return json(orders)
}
export const POST: RequestHandler = async ({ request, locals }) => {
const body = await request.json()
const order = await db.order.create({ data: { ...body, userId: locals.user.id } })
return json(order, { status: 201 })
}
State Management Patterns
Loader Data as State (Primary)
<script>
let { data } = $props()
// data.orders, data.user from load function
</script>
Form State with use:enhance
<script>
import { enhance } from '$app/forms'
let { form, data } = $props()
</script>
<form method="POST" use:enhance>
<input name="name" bind:value={form?.name} />
<button type="submit">Save</button>
</form>
Client State with Stores
// src/lib/stores/cart.svelte.ts
import { writable } from 'svelte/store'
export const cart = writable<CartItem[]>([])
Performance Optimization
- Compiles to vanilla JS — no virtual DOM, ~5KB runtime
- Per-component hydration reduces initial JS cost
- Load functions run on server for SSR, client for SPA navigation
- Route-level code splitting by default
preloadattributes on critical assets- Cache headers via
setHeadersin load functions
Build & Bundle Considerations
Adapter Configuration
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel' // or -node, -netlify, -cloudflare
export default {
kit: {
adapter: adapter({
runtime: 'edge', // for adapter-vercel
regions: ['iad1'],
}),
prerender: {
entries: ['/', '/about', '/blog/*'],
},
},
}
Build Commands
npm run build # adapter-specific build
npm run preview # preview production build
npm run dev # dev server with HMR
Environment Variables
// Server-only: process.env.DATABASE_URL
// Public: import { env } from '$env/dynamic/public' or '$env/static/public'
// Private: import { env } from '$env/dynamic/private' or '$env/static/private'
Testing Strategies
Load Function Tests
import { describe, it, expect } from 'vitest'
import { load } from './+page.server'
it('returns orders for authenticated user', async () => {
const result = await load({ locals: { user: { id: '1' } }, url: new URL('http://localhost'), params: {} })
expect(result).toHaveProperty('orders')
expect(result).toHaveProperty('user')
})
Form Action Tests
it('validates form input', async () => {
const formData = new FormData()
formData.set('email', 'invalid')
const result = await actions.default({ request: new Request('http://localhost', { method: 'POST', body: formData }), locals: { user: { id: '1' } } })
expect(result.status).toBe(400)
})
E2E Tests
import { test, expect } from '@playwright/test'
test('submits contact form', async ({ page }) => {
await page.goto('/contact')
await page.fill('[name="email"]', 'test@test.com')
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/thanks/)
})
Migration Patterns
Svelte 4 Stores to Svelte 5 Runes
// Svelte 4
import { writable, derived } from 'svelte/store'
export const count = writable(0)
// Svelte 5
let count = $state(0)
Express to SvelteKit
| Express + SPA | SvelteKit |
|---|---|
| Express routes | +page.svelte + +page.server.ts |
| REST API | +server.ts |
| Session middleware | hooks.server.ts |
| Client fetch | load() functions |
Anti-Patterns
- Fetching on client for initial data — use load()
- Mutating $page.data — read-only
- Not throwing redirect —
throw redirect(), notreturn redirect() - Large layout loads — keep minimal
- Missing +error.svelte — every app needs one
- Not using fail() for validation — use status 400
Common Pitfalls
- Fetching on client for initial data: Use
+page.server.tsload(), not onMount. - Mutating $page.data directly: Read-only. Use stores or form actions.
- Forgetting
throw redirect: Must be thrown, not returned. - Over-fetching in layout load: Keep minimal — runs on every navigation.
- Mixing server and client code:
$page,$app/storesare client-only. - Missing error pages: Add
+error.svelte.
Compared With
| Aspect | SvelteKit | Next.js App Router | Nuxt 3 |
|---|---|---|---|
| Data loading | load functions | async component + fetch | useAsyncData |
| Mutations | form actions | Server Actions | useFetch with method |
| API endpoints | +server.ts | route.ts | server/api/ |
| Bundle size | ~5KB runtime | ~70KB+ (React) | ~40KB (Vue) |
| Hydration | Per-component | Full-page | Full-page |
Ecosystem & Tooling
npm create svelte@latest— scaffoldnpm run dev— HMRnpm run build— adapter buildsvelte-check— CLI type checking@sveltejs/adapter-auto— automatic adaptersvelte-add— add integrations
Workflow
Step 1: Route Structure
src/routes/
+page.svelte -- /
+layout.svelte -- root layout
+layout.server.ts -- shared data
orders/
+page.svelte -- /orders
+page.server.ts -- orders load
[id]/
+page.svelte -- /orders/:id
+page.server.ts
api/
orders/
+server.ts -- /api/orders
Step 2: Page Load
export const load: PageServerLoad = async ({ locals, url }) => {
return { orders: await db.order.findMany({ where: { userId: locals.user.id } }) }
}
Step 3: Form Actions
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData()
// validate, process
throw redirect(303, '/success')
}
}
Step 4: API Endpoints
export const GET: RequestHandler = async () => {
return json(await db.product.findMany())
}
Step 5: Hooks
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.cookies.get('session'))
return await resolve(event)
}
Rules
- All routes follow file-based naming (+page.svelte, +layout.svelte, +server.ts).
- Data fetching in load functions, never onMount for initial data.
- Form mutations use Actions with fail() for errors, redirect() for success.
- Server-only code in lib/server/ — never import in client.
- Stores for client state only; server state flows through load functions.
- Always set cache headers for public data.
References
- references/endpoints-loading.md
- references/stores-context.md
- references/sveltekit-auth.md
- references/sveltekit-data.md
- references/sveltekit-deployment.md
- references/sveltekit-routing.md
- references/sveltekit-form-actions.md
- references/sveltekit-deployment-adapters.md
Handoff
Hand off to frontend/universal/state-management/SKILL.md or frontend/universal/performance/SKILL.md.
Implementation Patterns
Factory Pattern for Module Creation
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
Builder Pattern for Complex Configuration
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
Production Considerations
Deployment Checklist
- Production build with optimizations enabled
- Environment variables configured per environment
- Health check endpoint responds correctly
- Error tracking and monitoring integrated
- Logging level configured (not debug in production)
- Resource limits configured
- Database migrations applied
- Static assets built and served from CDN or cache
- Feature flags toggled appropriately
- Rollback plan documented and tested
Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Architecture Decision Trees
Data Loading Decision Tree
Does the data change on every request?
├── No → Can it be static at build time?
│ ├── Yes → `export const prerender = true` + PageData build
│ └── No → Is it user-specific?
│ ├── Yes → load() function with fetch() + cookies
│ └── No → load() with `+page.server.ts` for DB access
└── Yes → Should it be cached?
├── Yes → `load()` with `Cache-Control` headers or `+page.server.ts` cached fetch
└── No → `load()` with per-request fresh data
Form Handling Decision Tree
Is the form complex (multi-step, file uploads)?
├── No → Simple form with bind:value + <form> + use:enhance
└── Yes → Does it need optimistic updates?
├── Yes → use:enhance with custom callback for optimistic UI
└── No → +page.server.ts with form actions (default, login, register)
Need file uploads?
├── Yes → multipart/form-data + server action + file validation
└── No → JSON-based form data via FormData