Claude Code with Svelte and SvelteKit: Developer Workflow
Why SvelteKit projects need explicit Claude Code configuration
Claude Code knows Svelte. It knows the component structure, the lifecycle functions, the store primitives, and SvelteKit's file-based routing. It can generate a working component from a brief prompt.
The problem is timing. Claude Code's training data spans both Svelte 4 and Svelte 5, and without explicit direction it blends them. You ask for a reactive variable and get let count = $state(0) in one component and let count = 0 with a $: label in the next. You ask for a store and get a Svelte 4 writable where a Svelte 5 rune or a class-based state object would be correct for your project. Each inconsistency is small. Across a codebase they accumulate into a maintenance burden.
A project-specific CLAUDE.md eliminates this ambiguity. You specify the Svelte version, the reactivity model, the SvelteKit conventions, and the testing setup once. Claude Code applies them consistently from the first component to the last. This guide builds that configuration and covers the workflows that follow from it. If you are new to Claude Code, the Claude Code setup guide covers installation before any project configuration applies.
The SvelteKit CLAUDE.md
A SvelteKit CLAUDE.md needs to answer more questions than a generic frontend config because Svelte 5 runes and SvelteKit's data-loading model introduce decisions that Claude Code must navigate correctly from the start.
# SvelteKit project rules
## Project versions
- Svelte 5 (runes mode). Not Svelte 4.
- SvelteKit with file-based routing in src/routes/.
- TypeScript throughout. lang="ts" on all script blocks.
## Svelte 5 reactivity (runes, use exclusively)
- Reactive state: $state()
- Derived values: $derived() or $derived.by(() => { ... }) for complex derivations
- Side effects: $effect()
- Props: $props() with destructuring
- No $: reactive declarations. No $: labels of any kind.
- No Svelte 4 writable/readable/derived stores for internal component state.
Svelte stores are permitted only for cross-component shared state accessed
outside of component context (e.g., in utility modules).
## Component conventions
- File: src/lib/components/{ComponentName}.svelte
- Script block first, then template, then style.
- Props destructured from $props(): let { label, onClick } = $props<{ label: string; onClick: () => void }>()
- Events replaced by callback props in Svelte 5. No createEventDispatcher.
- Scoped styles in <style> block. No inline style attributes.
- One component per file. Extract when a second component appears.
## SvelteKit data loading
- Page data: +page.server.ts for server-only loads (database, auth-protected APIs)
- +page.ts for loads that can run on client and server (public API calls)
- Layout data: +layout.server.ts or +layout.ts following the same rule
- Typed with PageServerLoad / PageLoad from ./$types
## SvelteKit mutations
- Form actions in +page.server.ts for form submissions and mutations
- use:enhance on all forms to progressively enhance without full reload
- Actions return fail() for validation errors and redirect() for success
- No API routes for mutations that are only called from this application
## File structure
- Routes: src/routes/
- Shared components: src/lib/components/
- Utility functions: src/lib/utils/
- Types: src/lib/types/{domain}.ts
- Stores (cross-component only): src/lib/stores/{name}.ts
- Path alias: $lib maps to src/lib/
## Testing
- Framework: Vitest + Svelte Testing Library (@testing-library/svelte)
- Run full suite: npx vitest run
- Watch mode: npx vitest
- E2E: Playwright for full-stack flows including form actions
- Test files: colocated {ComponentName}.test.ts for components
- Test user-visible behaviour. No assertions against internal rune values.
## Hard rules
- No any types. TypeScript strict mode is on.
- Run npx svelte-check after every structural change.
- No Svelte 4 syntax in Svelte 5 components.
- All images need alt text. Decorative images: alt=""
The version pin and the explicit runes-only rule are the two highest-value lines in this config. Without them, Claude Code will produce syntactically correct Svelte that uses the wrong reactive model for your project, and svelte-check will not catch it because both models are valid Svelte.
The component authoring loop
The workflow that produces the most consistent output: requirements to component with typed props and colocated tests, all in one pass.
"Create a SearchInput component in src/lib/components/. It receives a value: string prop and an onSearch: (query: string) => void callback prop. On input, debounce the call to onSearch by 300ms. Show a clear button when value is non-empty that resets the input. Follow the project component conventions."
With the CLAUDE.md in place, Claude Code will:
- Create
SearchInput.sveltewith<script lang="ts"> - Destructure props from
$props<{ value: string; onSearch: (query: string) => void }>() - Use
$state()for the internal debounce timer, not a$:label - Use a callback prop for the clear action rather than
createEventDispatcher - Scope all styles in the
<style>block - Create
SearchInput.test.tsthat tests the 300ms debounce and the clear button behaviour
The output changes significantly without the CLAUDE.md. You get export let value and $: query = value and a dispatched search event. It works. It is not Svelte 5.
Iterating on components
Revisions work best when you describe the new behaviour rather than the code change. "The clear button should not appear while the search is pending (loading prop is true)" is unambiguous. "Hide the button during loading" is interpreted relative to whatever state management Claude currently sees.
For template logic involving reactive conditions, describe the state values that determine each branch. Claude Code maps explicit state conditions to {#if} blocks correctly. It maps implicit conditions inconsistently.
SvelteKit load functions
Load functions are the data layer of SvelteKit. Getting them right determines whether a page is server-rendered correctly, whether data is available at the right time, and whether TypeScript types flow from the load to the component.
The distinction your CLAUDE.md encodes: +page.server.ts for database access and protected APIs, +page.ts for public API calls that can run on both client and server.
Prompting Claude Code for a load function:
"Create a +page.server.ts load function for the product detail page at src/routes/products/[id]/. It should fetch the product from the database using the Drizzle client imported from $lib/db.ts. If the product is not found, throw error(404, 'Product not found'). Return the product typed with the Product type from $lib/types/product.ts. Use the PageServerLoad type from ./$types."
Claude Code generates a typed load function that uses the Drizzle client correctly, handles the not-found case, and infers the return type so the component's data prop is fully typed without a manual type annotation.
The type flow from load to component is one area where Svelte 5 and SvelteKit together are particularly strong. Add this to your CLAUDE.md to enforce the pattern:
## Load function type flow
- Load function return type is always inferred, not manually specified
- Component receives data via: let { data } = $props<{ data: PageData }>()
- PageData is imported from ./$types, not manually defined
- Never manually type the data prop shape in the component
This keeps load and component in sync automatically. When the load function return shape changes, the component's TypeScript errors point directly to the mismatch.
Form actions for mutations
Form actions are the correct mutation pattern for SvelteKit. They run on the server, integrate with SvelteKit's progressive enhancement model, and avoid the boilerplate of a separate API route for operations that are only called from within the application.
The prompt that produces reliable output:
"Create form actions in src/routes/products/[id]/+page.server.ts. Add an update action that receives name and price from FormData, validates with Zod (name is required, price must be a positive number), updates the product in the database, and returns { success: true }. If validation fails, return fail(422, { errors: ... }). Add a delete action that removes the product and redirects to /products."
Claude Code generates both actions with the correct Actions type import, Zod validation, Drizzle database calls, fail() for validation errors, and redirect() for the success case. The +page.svelte that consumes these actions should use use:enhance for progressive enhancement:
<script lang="ts">
import { enhance } from '$app/forms'
let { data, form } = $props<{ data: PageData; form: ActionData }>()
</script>
<form method="POST" action="?/update" use:enhance>
<input name="name" value={data.product.name} />
<input name="price" type="number" value={data.product.price} />
{#if form?.errors?.name}
<p class="error">{form.errors.name}</p>
{/if}
<button type="submit">Update</button>
</form>
The use:enhance directive prevents full-page reloads on submission, runs the action, and re-runs the load function to refresh the page data. Add the use:enhance pattern to your CLAUDE.md so Claude Code includes it automatically:
- All forms use method="POST" with use:enhance from $app/forms
- No JavaScript fetch calls for form submissions handled by actions
Shared state across components
Svelte 5 runes handle component-local state. For state that needs to be shared across components without prop drilling, you have two options: a Svelte store (if the state is accessed outside of component context) or a shared $state object exported from a module (if the state is always accessed from within components).
The module-exported $state pattern works well for shared client-side state in Svelte 5:
// src/lib/stores/cart.svelte.ts
export const cart = $state({
items: [] as CartItem[],
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
})
export function addItem(item: CartItem) {
const existing = cart.items.find(i => i.id === item.id)
if (existing) {
existing.quantity += 1
} else {
cart.items.push(item)
}
}
This file uses $state at the module level, which is valid in Svelte 5. Components import cart and addItem directly. No store subscription boilerplate, no $ prefix in templates.
Add the pattern to CLAUDE.md so Claude reaches for it over writable stores for shared client state:
## Shared state pattern
- Cross-component shared state: module-level $state in src/lib/stores/{name}.svelte.ts
- Svelte writable/readable stores: only for state accessed in non-component contexts
(utility functions, server-side code, contexts that cannot use runes)
Testing SvelteKit applications
Testing is the area where most SvelteKit Claude Code workflows are weakest. Server load functions cannot be tested directly with Svelte Testing Library because they run outside the component model. Form actions have the same constraint. The correct split is Playwright for full-stack flows and Vitest with Svelte Testing Library for component and utility unit tests.
Component tests focus on rendered output and user interaction:
import { render, screen } from '@testing-library/svelte'
import userEvent from '@testing-library/user-event'
import SearchInput from './SearchInput.svelte'
test('calls onSearch after 300ms debounce', async () => {
const onSearch = vi.fn()
render(SearchInput, { value: '', onSearch })
await userEvent.type(screen.getByRole('textbox'), 'query')
await vi.runAllTimersAsync()
expect(onSearch).toHaveBeenCalledWith('query')
})
Playwright tests cover the server-rendered data layer:
test('product detail page loads and updates correctly', async ({ page }) => {
await page.goto('/products/123')
await expect(page.getByRole('heading')).toContainText('Widget')
await page.fill('[name="price"]', '29.99')
await page.click('button:has-text("Update")')
await expect(page.getByText('29.99')).toBeVisible()
})
Add the split explicitly to your CLAUDE.md:
## Test scope
- Vitest: components, utility functions, shared state modules
- Playwright: load functions, form actions, full-page flows
- Do not test load functions or form actions with Vitest directly
Claude Code follows this routing when it is explicit. Without it, it attempts to mock SvelteKit internals in Vitest, which is fragile and inconsistent. The Claude Code hooks guide covers how to automate test runs as part of the development loop so tests fire without a manual step.
What Svelte developers get wrong first
Three failure modes appear consistently when Svelte developers start using Claude Code without framework-specific configuration.
Svelte 4 syntax in a Svelte 5 project. Without the version pin and runes-only rule, Claude Code produces $: derived = value * 2 and let items = writable([]) alongside $state. The output passes svelte-check and runs, but it is inconsistent with the rest of the project. The CLAUDE.md rule "No $: reactive declarations. Svelte 5 runes exclusively" prevents this with one line.
Using API routes for form submissions. Developers coming from Next.js or plain Express reach for API routes by default. Claude Code mirrors this pattern when it appears in the codebase. SvelteKit's form actions are strictly better for internal mutations: they work without JavaScript, integrate with use:enhance for progressive enhancement, and have direct access to the platform's request context. The CLAUDE.md rule "No API routes for mutations called only from this application" routes Claude to actions correctly.
Not specifying the load function type. Without a clear rule, Claude Code sometimes manually types the data prop in the component instead of importing PageData from ./$types. This breaks type inference from the load function and creates a manual sync point that drifts over time. The CLAUDE.md type flow rules above fix this.
Building a consistent SvelteKit workflow
The configuration in this guide produces a setup where Claude Code generates Svelte 5 rune-based components consistently, load functions that type-flow into components correctly, form actions that use progressive enhancement, and tests that split correctly between Vitest and Playwright.
The foundation is the CLAUDE.md template above. Add it to your project root, run one full feature cycle (load function, form action, component, Playwright test), and adjust the rules based on what Claude generates. Two or three additional rules typically emerge from the first session. After that, the output is reliable enough to deploy with confidence.
The Claude Code best practices guide covers the session management and context window techniques that keep large SvelteKit projects performing well. The Claude Code custom agents guide covers how to build specialised subagents for tasks like accessibility auditing and performance profiling that run alongside your main development session.
If you are evaluating Claude Code against other AI coding tools for a SvelteKit project, the Claude Code vs Cursor comparison covers the trade-offs in practical terms. For what a Claudify subscription adds on top, including pre-configured SvelteKit skills, see the Claudify plans.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify