Claude Code for SvelteKit: Advanced Patterns for Full-Stack Apps — Claude Skills 360 Blog
Blog / Frontend / Claude Code for SvelteKit: Advanced Patterns for Full-Stack Apps
Frontend

Claude Code for SvelteKit: Advanced Patterns for Full-Stack Apps

Published: November 21, 2026
Read time: 9 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free