Why Svelte and SvelteKit Need Specific AI Rules
Svelte is unique among frontend frameworks — it's a compiler, not a runtime library. There's no virtual DOM, no React.createElement, no runtime framework code shipped to the browser. And Svelte 5 introduced runes — a new reactivity system that replaces Svelte 4's $: reactive declarations with explicit $state, $derived, and $effect primitives. AI assistants trained on Svelte 3/4 content generate the old syntax.
SvelteKit adds another dimension: it's Svelte's full-stack framework (like Next.js for React) with file-based routing, server-side rendering, load functions for data fetching, form actions for mutations, and server routes for APIs. AI confuses SvelteKit patterns with Next.js patterns, generating getServerSideProps-style code that doesn't exist.
These rules target Svelte 5 with SvelteKit 2. The runes syntax is the primary upgrade — every reactive pattern changed. Specify your Svelte version clearly.
Rule 1: Svelte 5 Runes for Reactivity
The rule: 'Use Svelte 5 runes for all reactivity. $state() for reactive state: let count = $state(0). $derived for computed values: let doubled = $derived(count * 2). $effect for side effects: $effect(() => { console.log(count) }). $props() for component props. Never use Svelte 4 syntax: no $: reactive declarations, no export let for props, no reactive stores with $ prefix auto-subscription.'
For component props: 'Use $props() to declare props: let { name, age = 25 } = $props<{ name: string; age?: number }>(). This replaces export let name; export let age = 25;. Use $bindable() for two-way binding props. Type props with TypeScript generics on $props<T>().'
Runes are the biggest change in Svelte's history. AI generates $: doubled = count * 2 (Svelte 4) instead of let doubled = $derived(count * 2) (Svelte 5). The old syntax still works in Svelte 5's compatibility mode but is deprecated — your rules should enforce runes exclusively.
- $state() for reactive state — replaces let x = 0 with reactive assignment
- $derived for computed — replaces $: computed = x * 2
- $effect for side effects — replaces $: { sideEffect() }
- $props() for component props — replaces export let prop
- $bindable() for two-way binding — replaces bind:prop pattern
- No Svelte 4 syntax: no $: declarations, no export let, no $ auto-subscribe
AI generates $: doubled = count * 2 (Svelte 4). Correct: let doubled = $derived(count * 2) (Svelte 5). The old syntax works in compatibility mode but is deprecated. Enforce runes exclusively.
Rule 2: SvelteKit Load Functions
The rule: 'Use load functions in +page.ts (universal) or +page.server.ts (server-only) for all data fetching. Load functions receive event with params, url, fetch, and parent. Return data as a plain object — SvelteKit passes it to the page as data prop. Use +page.server.ts for data that needs server-only access (database, secrets). Use +page.ts for data that can run on both server and client.'
For cascading data: 'Use +layout.server.ts load functions for data shared across child routes (user session, nav items). Child load functions access parent data via await parent(). Don't refetch in children what the layout already provides.'
For streaming: 'Return promises in load functions for streaming: return { streamed: { comments: fetchComments() } }. The page renders immediately with the resolved data, and streamed data arrives later via Suspense-like behavior. Use this for non-critical data that shouldn't block the initial render.'
Return promises in load functions for streaming. The page renders with resolved data immediately, and streamed data (comments, recommendations) arrives later. Don't block the initial render on slow data.
Rule 3: Form Actions for Mutations
The rule: 'Use SvelteKit form actions for all data mutations. Define actions in +page.server.ts: export const actions = { default: async ({ request }) => { ... } }. Use named actions for multiple forms: actions = { login: async () => {}, register: async () => {} }. Use the enhance action from $app/forms for progressive enhancement: <form method="POST" use:enhance>.'
For validation: 'Validate form data in the action with Zod or superforms. Return validation errors as { status: 400, data: { errors } }. Access returned data in the page with form prop from $props(). Use superforms for type-safe form handling with built-in validation, constraints, and error display.'
Form actions are SvelteKit's answer to server actions in Next.js — but they came first and work without JavaScript. Progressive enhancement means the form submits as a standard POST when JS is disabled. AI generates fetch-based API calls instead of form actions — this rule channels all mutations through the correct pattern.
SvelteKit form actions work without JavaScript — progressive enhancement built in. AI generates fetch-based API calls instead. If a human submits a form, use a form action. If a machine calls an API, use a server route.
Rule 4: Server Routes and API Endpoints
The rule: 'API endpoints live in +server.ts files. Export named functions matching HTTP methods: export async function GET({ params, url }): Promise<Response>, export async function POST({ request }): Promise<Response>. Return new Response() or json() from @sveltejs/kit. Use these for: external API consumption (webhooks, third-party integrations), non-HTML responses (JSON APIs, file downloads), and WebSocket upgrade points.'
For when to use server routes vs form actions: 'Form actions for HTML form submissions with progressive enhancement. Server routes for programmatic API calls, JSON APIs consumed by external services, and non-form mutations (file uploads, WebSocket). If a human submits a form, use a form action. If a machine calls an API, use a server route.'
For hooks: 'Use hooks.server.ts for request-level middleware: authentication, logging, CORS, rate limiting. The handle function wraps every request. Use handleError for global error handling. Use handleFetch for customizing server-side fetch behavior.'
Rule 5: Component Patterns
The rule: 'Use .svelte files for all components. Keep components small — under 100 lines. Use {#snippet} blocks (Svelte 5) for reusable template fragments — replaces slots for many use cases. Use {#each} with keyed blocks: {#each items as item (item.id)}. Use transition directives for animations: transition:fade. Never manipulate the DOM directly — use binds and reactive statements.'
For styling: 'Use <style> blocks (scoped by default in Svelte). Use :global() sparingly for targeting child components. Use CSS custom properties for theming: --color-primary. If using Tailwind, use it consistently — never mix Tailwind and scoped styles in the same component.'
For stores (when needed alongside runes): 'Use Svelte stores for state shared across components that aren't in a parent-child relationship. Use writable() for mutable shared state, readable() for read-only shared state, derived() for computed shared state. Access with $store syntax in Svelte files.'
- Components under 100 lines — extract subcomponents for complexity
- {#snippet} for reusable template fragments (Svelte 5)
- {#each} with keys: {#each items as item (item.id)}
- Scoped <style> by default — :global() only when necessary
- transition: directives for animations — no manual DOM manipulation
- Stores for cross-component state — $store auto-subscription in templates
Complete Svelte/SvelteKit Rules Template
Consolidated rules for Svelte 5 + SvelteKit 2 projects.
- Svelte 5 runes: $state, $derived, $effect, $props — no Svelte 4 $: syntax
- Load functions in +page.server.ts — streaming for non-critical data
- Form actions for mutations — use:enhance for progressive enhancement
- Server routes (+server.ts) for JSON APIs — form actions for HTML forms
- hooks.server.ts for request middleware — auth, logging, CORS
- {#snippet} for template reuse — {#each (key)} — scoped styles
- superforms for type-safe form handling with Zod validation
- Vitest + @testing-library/svelte — Playwright for e2e