SvelteKit’s combination of file-based routing, co-located server code, and Svelte’s minimal runtime makes it one of the most productive full-stack frameworks available. Svelte 5 Runes replace the old reactive declarations with a simpler, explicit API. Claude Code generates SvelteKit code in the modern Runes style — no legacy $: reactive statements.
This guide covers SvelteKit with Claude Code: Svelte 5 Runes, form actions, load functions, server-side rendering, and deployment.
Project Setup
Set up a SvelteKit project with TypeScript, Svelte 5 Runes,
and Drizzle ORM for the database. I want server-side rendering.
## SvelteKit Project Conventions
- SvelteKit 2.x with Svelte 5 Runes (no legacy reactive statements)
- TypeScript strict mode
- File-based routing in src/routes/
- Server code: +page.server.ts (load + actions), +server.ts (API routes)
- Global state: Svelte 5 $state in src/lib/stores/ with context API for SSR safety
- Database: Drizzle ORM with PostgreSQL
- Form validation: superforms with zod schemas
- Styling: Tailwind CSS v4
- Testing: Vitest + @testing-library/svelte
- Deploy target: Cloudflare Pages (@sveltejs/adapter-cloudflare)
Svelte 5 Runes
Create a ProductCard component with add-to-cart functionality.
Use Svelte 5 Runes — no $: reactive statements.
<!-- src/lib/components/ProductCard.svelte -->
<script lang="ts">
import { cartStore } from '$lib/stores/cart.svelte';
interface Props {
product: {
id: string;
name: string;
price: number;
imageUrl: string;
inStock: boolean;
};
class?: string;
}
let { product, class: className = '' }: Props = $props();
// Local reactive state with $state
let quantity = $state(1);
let isAdding = $state(false);
let justAdded = $state(false);
// Derived state with $derived
let totalPrice = $derived(product.price * quantity);
let canAdd = $derived(product.inStock && quantity > 0);
async function addToCart() {
if (!canAdd || isAdding) return;
isAdding = true;
try {
await cartStore.addItem({ productId: product.id, quantity });
justAdded = true;
setTimeout(() => (justAdded = false), 2000);
} finally {
isAdding = false;
}
}
</script>
<article class="product-card {className}">
<img src={product.imageUrl} alt={product.name} loading="lazy" />
<div class="details">
<h3>{product.name}</h3>
<p class="price">${(product.price / 100).toFixed(2)}</p>
{#if product.inStock}
<div class="quantity-control">
<button
onclick={() => (quantity = Math.max(1, quantity - 1))}
aria-label="Decrease quantity"
disabled={quantity <= 1}
>−</button>
<span aria-label="Quantity">{quantity}</span>
<button
onclick={() => (quantity += 1)}
aria-label="Increase quantity"
>+</button>
</div>
<p class="total">Total: ${(totalPrice / 100).toFixed(2)}</p>
<button
onclick={addToCart}
disabled={!canAdd || isAdding}
class:success={justAdded}
>
{#if isAdding}
Adding...
{:else if justAdded}
Added ✓
{:else}
Add to cart
{/if}
</button>
{:else}
<p class="out-of-stock">Out of stock</p>
{/if}
</div>
</article>
Key Svelte 5 differences from Svelte 4: $props() replaces export let, $state() replaces regular variables for reactivity, $derived() replaces $: computed, onclick replaces on:click.
SvelteKit Load Functions
The product page needs server-side rendering.
Load the product, check if it's in the user's wishlist,
and handle 404 if not found.
// src/routes/products/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals }) => {
const product = await db.query.products.findFirst({
where: (products, { eq }) => eq(products.slug, params.slug),
with: {
category: true,
images: true,
},
});
if (!product) {
error(404, { message: `Product "${params.slug}" not found` });
}
// Load wishlist status in parallel (don't block if not authenticated)
const wishlistPromise = locals.user
? db.query.wishlistItems.findFirst({
where: (items, { and, eq }) => and(
eq(items.userId, locals.user!.id),
eq(items.productId, product.id),
),
})
: Promise.resolve(null);
const [inWishlist] = await Promise.all([wishlistPromise]);
return {
product,
inWishlist: !!inWishlist,
// User data from locals (set by hooks.server.ts)
user: locals.user ?? null,
};
};
<!-- src/routes/products/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// Data from load function is typed
let { product, inWishlist, user } = $derived(data);
</script>
<svelte:head>
<title>{product.name} | My Store</title>
<meta name="description" content={product.description} />
</svelte:head>
<h1>{product.name}</h1>
<p class="price">${(product.price / 100).toFixed(2)}</p>
Form Actions for Mutations
The wishlist add/remove toggle should work without JavaScript (progressive enhancement).
Use SvelteKit form actions.
// src/routes/products/[slug]/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
toggleWishlist: async ({ locals, params, request }) => {
if (!locals.user) {
// Redirect to login with return URL
redirect(303, `/login?redirect=/products/${params.slug}`);
}
const product = await db.query.products.findFirst({
where: (p, { eq }) => eq(p.slug, params.slug),
});
if (!product) return fail(404, { message: 'Product not found' });
const existing = await db.query.wishlistItems.findFirst({
where: (items, { and, eq }) => and(
eq(items.userId, locals.user!.id),
eq(items.productId, product.id),
),
});
if (existing) {
await db.delete(wishlistItems).where(eq(wishlistItems.id, existing.id));
return { inWishlist: false };
} else {
await db.insert(wishlistItems).values({
userId: locals.user.id,
productId: product.id,
});
return { inWishlist: true };
}
},
};
<!-- In the page component — works without JS via native form submission -->
<form method="POST" action="?/toggleWishlist">
<button type="submit">
{inWishlist ? '♥ Remove from wishlist' : '♡ Add to wishlist'}
</button>
</form>
<!-- With enhance — progressive enhancement adds JS interactivity -->
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" action="?/toggleWishlist" use:enhance>
<button type="submit">{inWishlist ? '♥ Remove' : '♡ Add'}</button>
</form>
use:enhance intercepts the form submit, makes it a fetch request, and updates the page data — no full page reload. Falls back to standard form submission if JS fails.
Deploying to Cloudflare Pages
Deploy this SvelteKit app to Cloudflare Pages.
I need: edge functions, KV for caching, D1 for the database.
npm i -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>'],
},
}),
},
};
// src/app.d.ts — types for Cloudflare bindings
declare global {
namespace App {
interface Platform {
env: {
DB: D1Database; // Cloudflare D1 SQL
KV: KVNamespace; // Cloudflare KV store
ASSETS: Fetcher;
};
context: ExecutionContext;
}
}
}
// Accessing Cloudflare bindings in load functions
export const load: PageServerLoad = async ({ platform }) => {
const db = drizzle(platform!.env.DB); // D1 via Drizzle
const cached = await platform!.env.KV.get('some-key'); // KV cache
};
For the Cloudflare Workers and Pages deployment architecture beyond SvelteKit, see the serverless guide. For comparing SvelteKit with Next.js and Vue/Nuxt for your specific project, see the Claude Skills 360 bundle which includes framework decision-making skill sets. For authentication patterns in SvelteKit, see the authentication guide. Start with the free tier to try SvelteKit project generation.