SvelteKit is a full-stack framework built around Svelte’s compiler: fine-grained reactivity without a virtual DOM, file-based routing, and integrated server/client data loading. Svelte 5’s runes system ($state, $derived, $effect) replaces the older reactive syntax with explicit primitives. Claude Code generates SvelteKit load functions, form actions, streaming responses, server hooks for auth, and the adapter configuration for Cloudflare Pages or Vercel.
CLAUDE.md for SvelteKit Projects
## SvelteKit Stack
- SvelteKit 2.x with Svelte 5 (runes mode)
- TypeScript throughout — no `.js` route files
- Database: Drizzle ORM + PostgreSQL (via Neon serverless)
- Auth: Lucia v3 for session management
- Validation: Zod for form actions and API endpoints
- Adapter: @sveltejs/adapter-cloudflare for Cloudflare Pages
- Styling: Tailwind CSS with shadcn-svelte components
- State: Svelte 5 runes ($state, $derived, $effect) — no stores for local state
Load Functions (+page.server.ts)
// src/routes/orders/+page.server.ts
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { orders, orderItems } from '$lib/server/schema';
import { eq, desc, and } from 'drizzle-orm';
import { fail, redirect } from '@sveltejs/kit';
import { z } from 'zod';
export const load: PageServerLoad = async ({ locals, url }) => {
// locals.user set by hooks.server.ts
if (!locals.user) throw redirect(302, '/login');
const page = Number(url.searchParams.get('page') ?? '1');
const status = url.searchParams.get('status') ?? undefined;
const limit = 20;
const offset = (page - 1) * limit;
const conditions = [eq(orders.userId, locals.user.id)];
if (status) conditions.push(eq(orders.status, status));
const [userOrders, totalCount] = await Promise.all([
db.query.orders.findMany({
where: and(...conditions),
orderBy: desc(orders.createdAt),
limit,
offset,
with: {
items: { with: { product: { columns: { name: true, imageUrl: true } } } },
},
}),
db.$count(orders, and(...conditions)),
]);
return {
orders: userOrders,
pagination: {
page,
totalPages: Math.ceil(totalCount / limit),
total: totalCount,
},
};
};
Form Actions
// src/routes/orders/[id]/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail, error } from '@sveltejs/kit';
import { z } from 'zod';
const CancelOrderSchema = z.object({
reason: z.string().min(1, 'Reason is required').max(500),
confirm: z.literal('true'),
});
export const actions: Actions = {
// Named action: ?/cancel
cancel: async ({ request, params, locals }) => {
if (!locals.user) return fail(401, { message: 'Unauthorized' });
const formData = await request.formData();
const raw = {
reason: formData.get('reason'),
confirm: formData.get('confirm'),
};
const parsed = CancelOrderSchema.safeParse(raw);
if (!parsed.success) {
return fail(422, {
errors: parsed.error.flatten().fieldErrors,
values: raw,
});
}
const order = await db.query.orders.findFirst({
where: and(
eq(orders.id, params.id),
eq(orders.userId, locals.user.id),
),
});
if (!order) throw error(404, 'Order not found');
if (order.status !== 'pending') {
return fail(422, { message: 'Only pending orders can be cancelled' });
}
await db.update(orders)
.set({ status: 'cancelled', cancelReason: parsed.data.reason, updatedAt: new Date() })
.where(eq(orders.id, params.id));
return { success: true, message: 'Order cancelled successfully' };
},
// Default action: no name
default: async ({ request, locals }) => {
// form without ?/action uses default
},
};
Svelte 5 Runes Component
<!-- src/routes/orders/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
import { enhance } from '$app/forms';
import OrderCard from '$lib/components/OrderCard.svelte';
let { data }: { data: PageData } = $props();
// $state: reactive local state (replaces let x = value)
let selectedStatus = $state('');
let isLoading = $state(false);
// $derived: computed values (replaces $: derived = ...)
let filteredOrders = $derived(
selectedStatus
? data.orders.filter(o => o.status === selectedStatus)
: data.orders
);
let statusCounts = $derived(
data.orders.reduce((acc, order) => {
acc[order.status] = (acc[order.status] ?? 0) + 1;
return acc;
}, {} as Record<string, number>)
);
// $effect: side effects (replaces $: { sideEffect() })
$effect(() => {
// Runs whenever selectedStatus changes
document.title = selectedStatus
? `Orders (${selectedStatus})`
: 'All Orders';
});
</script>
<div class="orders-page">
<header>
<h1>Your Orders</h1>
<p class="subtitle">{data.pagination.total} orders total</p>
</header>
<!-- Status filter tabs -->
<div class="status-tabs" role="tablist">
{#each ['', 'pending', 'processing', 'shipped', 'delivered', 'cancelled'] as status}
<button
role="tab"
aria-selected={selectedStatus === status}
onclick={() => selectedStatus = status}
class:active={selectedStatus === status}
>
{status || 'All'}
{#if statusCounts[status] || status === ''}
<span class="count">{status ? statusCounts[status] ?? 0 : data.orders.length}</span>
{/if}
</button>
{/each}
</div>
<!-- Orders list -->
{#if filteredOrders.length === 0}
<p class="empty">No {selectedStatus} orders found.</p>
{:else}
<ul class="orders-list">
{#each filteredOrders as order (order.id)}
<li>
<OrderCard {order} />
</li>
{/each}
</ul>
{/if}
<!-- Pagination -->
{#if data.pagination.totalPages > 1}
<nav class="pagination" aria-label="Order pagination">
{#each Array.from({ length: data.pagination.totalPages }, (_, i) => i + 1) as pageNum}
<a
href="?page={pageNum}"
aria-current={pageNum === data.pagination.page ? 'page' : undefined}
class:current={pageNum === data.pagination.page}
>
{pageNum}
</a>
{/each}
</nav>
{/if}
</div>
Progressive Form Enhancement
<!-- Cancel order form with progressive enhancement -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
let isSubmitting = $state(false);
let showConfirm = $state(false);
</script>
<form
method="POST"
action="?/cancel"
use:enhance={() => {
isSubmitting = true;
return async ({ result, update }) => {
isSubmitting = false;
if (result.type === 'success') {
showConfirm = false;
}
await update();
};
}}
>
<div class="field">
<label for="reason">Cancellation reason</label>
<textarea
id="reason"
name="reason"
rows={3}
value={form?.values?.reason ?? ''}
aria-invalid={!!form?.errors?.reason}
aria-describedby={form?.errors?.reason ? 'reason-error' : undefined}
></textarea>
{#if form?.errors?.reason}
<p id="reason-error" class="field-error">{form.errors.reason[0]}</p>
{/if}
</div>
<input type="hidden" name="confirm" value="true" />
{#if form?.message}
<p class="form-error">{form.message}</p>
{/if}
<button type="submit" disabled={isSubmitting} class="btn-danger">
{isSubmitting ? 'Cancelling...' : 'Cancel Order'}
</button>
</form>
Hooks (Auth + CSRF)
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { lucia } from '$lib/server/auth';
import { sequence } from '@sveltejs/kit/hooks';
const authHandle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (!sessionId) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await lucia.validateSession(sessionId);
if (session?.fresh) {
// Refresh session cookie
const cookie = lucia.createSessionCookie(session.id);
event.cookies.set(cookie.name, cookie.value, { path: '/', ...cookie.attributes });
}
if (!session) {
event.cookies.delete(lucia.sessionCookieName, { path: '/' });
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
};
const securityHandle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html,
});
// Add security headers
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
};
export const handle = sequence(authHandle, securityHandle);
Streaming with defer
// src/routes/dashboard/+page.server.ts — stream slow data
import { defer } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
// Fast data: return immediately
const user = locals.user;
// Slow data: return as promise, render page while it loads
const analyticsPromise = fetchSlowAnalytics(user.id);
const recentOrdersPromise = db.query.orders.findMany({
where: eq(orders.userId, user.id),
limit: 5,
orderBy: desc(orders.createdAt),
});
return {
user,
// Immediate
recentOrders: await recentOrdersPromise,
// Streamed — page renders with a skeleton, data fills in
analytics: analyticsPromise,
};
};
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<!-- Renders immediately with user + recentOrders -->
<h1>Welcome, {data.user.name}</h1>
<!-- Streams in when promise resolves -->
{#await data.analytics}
<div class="analytics-skeleton">Loading analytics...</div>
{:then analytics}
<AnalyticsDashboard {analytics} />
{:catch error}
<p class="error">Failed to load analytics: {error.message}</p>
{/await}
For the Drizzle ORM queries powering SvelteKit’s server-side load functions, see the Drizzle ORM guide for schema definitions and relational query patterns. For deploying to Cloudflare with D1 instead of Neon, the Cloudflare D1 guide covers SQLite at the edge. The Claude Skills 360 bundle includes SvelteKit skill sets covering form actions, Svelte 5 runes, and streaming patterns. Start with the free tier to try SvelteKit load function generation.