SvelteKit
Project context
This is a SvelteKit project using Svelte 5 with the runes API. AI assistants frequently default to Svelte 4 patterns ($:, $$props); these rules exist to keep the codebase modern.
Stack
- SvelteKit 2+
- Svelte 5+ with runes (
$state,$derived,$effect,$props,$bindable) - TypeScript strict
- Tailwind CSS 4
- Vite (built-in to SvelteKit)
- Vitest + Playwright
- pnpm
Folder structure
src/
app.html
app.d.ts
hooks.server.ts — server hooks (auth, error handling)
hooks.client.ts — client hooks (rare)
lib/ — code reusable across routes; aliased as $lib
server/ — code that must never ship to the browser
routes/
+layout.svelte
+layout.ts — shared load
+page.svelte
+page.ts — universal load
+page.server.ts — server-only load
+server.ts — API endpoints
+error.svelte
blog/[slug]/
+page.svelte
+page.server.ts
static/ — public assets
Svelte 5 runes (use these, not Svelte 4 syntax)
$state(...)— reactive local state (replaceslet foo)$derived(...)— computed values (replaces$:)$effect(() => { ... })— side effects (replaces$:for effects)$props()— declares props (replacesexport let)$bindable()— for two-way bindings on props
<script lang="ts">
let { initial = 0, onchange } = $props<{ initial?: number; onchange?: (n: number) => void }>()
let count = $state(initial)
let doubled = $derived(count * 2)
$effect(() => {
onchange?.(count)
})
</script>
<button onclick={() => count++}>Count: {count} (×2 = {doubled})</button>
Loading data
+page.ts(universal) — runs on server then client; for public, cacheable data+page.server.ts(server only) — for DB calls, secrets, anything server-bound- Return data from
load(); SvelteKit hydratesdataprop on+page.svelte - Use
depends('app:foo')+invalidate('app:foo')for fine-grained re-fetching - Use
parent()to inherit data from layout loads — don't refetch
Form actions
// +page.server.ts
import type { Actions } from './$types'
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData()
// ... process, then redirect / return error / return success
},
}
- Use form actions for mutations — not raw
fetch()from client - Validate inputs at the server boundary (Zod / valibot)
- Use
fail(400, { message })for validation errors - Use
redirect(303, '/path')for post-success navigation
API endpoints (+server.ts)
Only use +server.ts for true API endpoints (consumed by external clients, webhooks). For internal mutations, use form actions.
Adapters
@sveltejs/adapter-vercelfor Vercel@sveltejs/adapter-cloudflarefor Cloudflare Pages / Workers@sveltejs/adapter-nodefor Node servers@sveltejs/adapter-staticfor fully static sites- Pick one in
svelte.config.js; don't ship multiple
Patterns to follow
- Co-locate everything in
src/routes—+page.svelte, its load, its server-only logic - Prefix server-only modules with
$lib/server/or use+page.server.ts - Use
app.d.tsto typeApp.Locals(request-scoped data) andApp.Error - Use
<form method="POST">for mutations — progressive enhancement
Patterns to avoid
export let foo— use$props()$:reactive statements — use$derivedor$effect$$props,$$restProps— use$props()and rest spread- Storing fetched data in
let— usedatafrom a load function - Calling
fetchinonMount— use a load function instead - Mutating store values directly across components — use writable / runes deliberately
Testing
- Vitest for unit tests (component tests via
@testing-library/svelte) - Playwright for end-to-end
- Use
$lib/server/test-helpers.tsfor test fixtures (will not bundle to browser)
Tooling
pnpm dev— Vite dev serverpnpm build— production build (uses adapter)pnpm preview— preview built outputpnpm test— Vitestpnpm test:e2e— Playwrightpnpm check—svelte-check --tsconfig ./tsconfig.json
AI behavioral rules
- Default to Svelte 5 runes; never propose
let foofor reactive state - Don't suggest
$:— use$derivedfor values,$effectfor side effects - Don't suggest
export let— use$props() - For mutations, use form actions; don't propose client-side
fetchas the default - Put server-only code in
$lib/server/or+page.server.tsso it never bundles to the browser - For data fetching in pages, use a
loadfunction — neveronMount+fetch - Run
pnpm checkandpnpm testbefore declaring a task done