Use Cases (Updated: 6/2/2026)

Claude Code for Svelte and SvelteKit Development

A practical Claude Code guide for Svelte/SvelteKit setup, runes, routing, form actions, tests, and safer AI-assisted edits.

Claude Code for Svelte and SvelteKit Development

Why Claude Code Fits Svelte and SvelteKit

Svelte is a UI framework that compiles declarative components into lean JavaScript. SvelteKit adds the application layer: filesystem routing, server rendering, load functions, form actions, endpoint routes, and deployment adapters. The official Svelte docs describe Svelte as useful for everything from standalone components to full-stack apps with SvelteKit.

Claude Code is an agentic coding tool that can read a repository, edit files, run commands, and work from the terminal, IDE, desktop, or browser. That matters for SvelteKit because one feature often spans src/routes, src/lib/components, $lib/server, generated route types, and tests. A good Claude Code session is not “make me an app”; it is “read these files, explain the route boundary, then make the smallest safe change.”

The main beginner traps are Svelte 5 runes and SvelteKit’s server/client split. Runes are the current reactive primitives: $props receives component inputs, $state declares mutable reactive state, $derived computes values from state, and $effect synchronizes with browser-only side effects. This article uses a small task-management example to show setup, components, shared state, routing, form actions, tests, safe prompts, use cases, and pitfalls.

flowchart LR
  A["Write a small requirement"] --> B["Ask Claude Code to inspect files"]
  B --> C["Edit Svelte components"]
  C --> D["Check SvelteKit load/actions"]
  D --> E["Run type checks and tests"]
  E --> F["Review git diff manually"]

Project Setup

For a new SvelteKit project, the current official path is the Svelte CLI: npx sv create my-app. It can scaffold TypeScript and common tooling. If you only need a standalone Svelte app on Vite, the Vite guide also supports the svelte-ts template. Vite’s current guide lists Node.js 20.19+ or 22.12+ for Vite itself, so update Node first if your package manager warns you.

Claude Code has several install paths, including native installers, package managers, and npm. The current docs describe it as a tool that can read your codebase, edit files, run commands, and integrate with your development tools. On Windows, choose native Windows or WSL based on where your project and Node toolchain live.

# Create a SvelteKit app
npx sv create claude-svelte-demo
cd claude-svelte-demo
npm install
npm run dev

# Start Claude Code from the project root
claude

Start with planning when the repository is new to you. Claude Code’s plan mode is designed for reading and proposing changes before editing source files.

/plan
I want to add a task list feature to this SvelteKit project.
First inspect src/routes and src/lib, then propose the files to change, the data flow, and the test plan.
Do not edit files yet.

You can also launch directly in plan mode:

claude --permission-mode plan

Project rules belong in CLAUDE.md or .claude/CLAUDE.md. Keep them factual: preferred package manager, npm run check, Svelte 5 runes, form-action conventions, and “do not commit unless asked.” Claude Code docs explain that project instructions are loaded as context, so clear rules work better than vague style advice.

Build Small Svelte 5 Components

In Svelte 5, props are received with $props(), local reactive state is declared with $state, and computed state is declared with $derived. A simple way to explain it to a beginner: $state is the value that changes, while $derived is the display value recalculated from it. Use $effect only when you need browser-only synchronization such as Canvas, a third-party widget, or local storage.

The following component is copy-pasteable as src/lib/components/TaskCard.svelte.

<!-- src/lib/components/TaskCard.svelte -->
<script lang="ts">
  type Task = {
    id: string;
    title: string;
    done: boolean;
    estimateMinutes: number;
    tags: string[];
  };

  let {
    task,
    onToggle
  }: {
    task: Task;
    onToggle: (id: string) => void;
  } = $props();

  let statusLabel = $derived(task.done ? 'Done' : 'Open');
  let estimateLabel = $derived(`${Math.ceil(task.estimateMinutes / 15) * 15} min block`);
</script>

<article class:done={task.done} class="task-card">
  <div>
    <p class="status">{statusLabel}</p>
    <h3>{task.title}</h3>
    <p>{estimateLabel}</p>
  </div>

  <ul aria-label="Tags">
    {#each task.tags as tag}
      <li>{tag}</li>
    {/each}
  </ul>

  <button type="button" aria-pressed={task.done} onclick={() => onToggle(task.id)}>
    {task.done ? 'Mark open' : 'Mark done'}
  </button>
</article>

<style>
  .task-card {
    display: grid;
    gap: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 0.5rem;
    padding: 1rem;
  }

  .done {
    background: #f2fff5;
  }

  .status {
    font-size: 0.875rem;
    font-weight: 700;
  }

  ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    list-style: none;
    padding: 0;
  }

  li {
    border-radius: 999px;
    background: #eef2ff;
    padding: 0.2rem 0.6rem;
  }
</style>

The prompt you give Claude Code should preserve the public contract:

Improve src/lib/components/TaskCard.svelte while keeping the current Svelte 5 runes syntax.
Constraints:
- Do not change the Task type or onToggle signature.
- Keep onclick and do not rewrite this to legacy on:click.
- Keep aria-pressed on the button.
- Improve only layout and empty-state resilience.
- After editing, suggest one component test.

That level of detail prevents accidental API drift. It also stops Claude from turning a small component polish task into a broad refactor across src/lib.

Shared State: Runes First, Stores When Needed

Svelte 5 supports .svelte.js and .svelte.ts files. They behave like ordinary modules, except that runes can be used inside them. This is useful for shared reactive state and reusable reactive logic. Stores from svelte/store are still valid, especially for complex async streams, manual subscriptions, or interop with libraries that already speak the store contract.

For task filters, a small .svelte.ts module is enough:

// src/lib/state/taskFilters.svelte.ts
export type TaskStatus = 'all' | 'open' | 'done';

export const taskFilters = $state({
  query: '',
  status: 'all' as TaskStatus,
  tag: ''
});

export function resetTaskFilters() {
  taskFilters.query = '';
  taskFilters.status = 'all';
  taskFilters.tag = '';
}
<!-- src/lib/components/TaskFilterPanel.svelte -->
<script lang="ts">
  import { resetTaskFilters, taskFilters } from '$lib/state/taskFilters.svelte';
</script>

<section aria-label="Task filters">
  <label>
    Keyword
    <input bind:value={taskFilters.query} placeholder="Invoice, article, review..." />
  </label>

  <label>
    Status
    <select bind:value={taskFilters.status}>
      <option value="all">All</option>
      <option value="open">Open</option>
      <option value="done">Done</option>
    </select>
  </label>

  <button type="button" onclick={resetTaskFilters}>Reset</button>
</section>

The common mistake is forgetting server rendering. window, document, and localStorage do not exist during SSR. If a setting must be persisted in the browser, guard it with browser from $app/environment.

// src/lib/state/theme.svelte.ts
import { browser } from '$app/environment';

export const themeState = $state({
  theme: 'system' as 'system' | 'light' | 'dark'
});

export function loadTheme() {
  if (!browser) return;
  const saved = localStorage.getItem('theme');
  if (saved === 'light' || saved === 'dark' || saved === 'system') {
    themeState.theme = saved;
  }
}

export function saveTheme(nextTheme: typeof themeState.theme) {
  themeState.theme = nextTheme;
  if (browser) localStorage.setItem('theme', nextTheme);
}

Ask Claude Code to preserve that boundary:

Add persistence to the theme state.
Use $app/environment browser guards for localStorage.
Do not import browser-only APIs into +page.server.ts or $lib/server modules.

Routing and Data Loading

SvelteKit uses filesystem routing. src/routes/about creates /about, and src/routes/tasks/[slug] creates a dynamic route with a slug parameter. Route files use the + prefix: +page.svelte, +page.server.ts, +layout.svelte, and +server.ts.

Server-only data access should stay in $lib/server. That keeps database logic, private environment variables, and privileged API calls out of browser bundles.

// src/lib/server/tasks.ts
export type Task = {
  id: string;
  slug: string;
  title: string;
  done: boolean;
  estimateMinutes: number;
  tags: string[];
};

const tasks: Task[] = [
  {
    id: 'task-1',
    slug: 'write-svelte-guide',
    title: 'Draft the SvelteKit article',
    done: false,
    estimateMinutes: 45,
    tags: ['writing', 'svelte']
  }
];

export async function getTaskBySlug(slug: string) {
  return tasks.find((task) => task.slug === slug) ?? null;
}
// src/routes/tasks/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { getTaskBySlug } from '$lib/server/tasks';

export const load: PageServerLoad = async ({ params }) => {
  const task = await getTaskBySlug(params.slug);

  if (!task) {
    error(404, 'Task not found');
  }

  return { task };
};
<!-- src/routes/tasks/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageProps } from './$types';

  let { data }: PageProps = $props();
</script>

<svelte:head>
  <title>{data.task.title} | Tasks</title>
</svelte:head>

<article>
  <p>{data.task.done ? 'Done' : 'Open'}</p>
  <h1>{data.task.title}</h1>
  <p>Estimate: {data.task.estimateMinutes} minutes</p>
</article>

A safe Claude Code prompt for routing work names the route and the boundary:

Read src/routes/tasks/[slug] and src/lib/server/tasks.ts.
Add a dueDate field to Task and render it on the detail page.
Keep server-only data access inside $lib/server.
Do not rename the [slug] route directory.
Run npm run check afterward.

Form Actions and Progressive Enhancement

SvelteKit form actions let +page.server.ts export actions so a normal <form method="POST"> can submit to the server. The docs emphasize that client-side JavaScript is optional, and use:enhance can progressively improve the interaction when JavaScript is available.

This contact form validates on the server, returns field errors with fail, and keeps submitted values visible after an error.

// src/routes/contact/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions = {
  default: async ({ request }) => {
    const formData = await request.formData();
    const values = {
      name: String(formData.get('name') ?? '').trim(),
      email: String(formData.get('email') ?? '').trim(),
      message: String(formData.get('message') ?? '').trim()
    };

    const errors: Record<string, string> = {};
    if (values.name.length < 2) errors.name = 'Enter at least 2 characters.';
    if (!values.email.includes('@')) errors.email = 'Check the email address.';
    if (values.message.length < 10) errors.message = 'Enter at least 10 characters.';

    if (Object.keys(errors).length > 0) {
      return fail(400, { values, errors });
    }

    console.log('New inquiry', values);
    return { success: true };
  }
} satisfies Actions;
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageProps } from './$types';

  let { form }: PageProps = $props();
</script>

{#if form?.success}
  <p role="status">Sent. We will reply within 1 to 3 business days.</p>
{/if}

<form method="POST" use:enhance>
  <label>
    Name
    <input name="name" value={form?.values?.name ?? ''} aria-invalid={!!form?.errors?.name} />
  </label>
  {#if form?.errors?.name}<p>{form.errors.name}</p>{/if}

  <label>
    Email
    <input name="email" type="email" value={form?.values?.email ?? ''} aria-invalid={!!form?.errors?.email} />
  </label>
  {#if form?.errors?.email}<p>{form.errors.email}</p>{/if}

  <label>
    Message
    <textarea name="message" rows="5" aria-invalid={!!form?.errors?.message}>{form?.values?.message ?? ''}</textarea>
  </label>
  {#if form?.errors?.message}<p>{form.errors.message}</p>{/if}

  <button type="submit">Send</button>
</form>

The key rules: do not use GET for side effects, do not expose private keys to the browser, and do not render untrusted HTML with {@html} unless it is sanitized. When asking Claude Code to update a form, explicitly say “keep server validation” and “works without JavaScript.”

Tests and Verification

The Svelte testing docs describe Vitest as a good fit for Vite and SvelteKit projects. For components, Testing Library keeps assertions close to user behavior. Use Playwright for full navigation, form submission, and auth flows.

// src/lib/components/TaskCard.test.ts
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import TaskCard from './TaskCard.svelte';

describe('TaskCard', () => {
  it('toggles the task when the button is clicked', async () => {
    let toggledId = '';

    render(TaskCard, {
      task: {
        id: 'task-1',
        title: 'Write the SvelteKit article',
        done: false,
        estimateMinutes: 45,
        tags: ['writing']
      },
      onToggle: (id) => {
        toggledId = id;
      }
    });

    await fireEvent.click(screen.getByRole('button', { name: 'Mark done' }));
    expect(toggledId).toBe('task-1');
  });
});
npm run check
npm run test
npm run build
git diff -- src/lib src/routes

Tell Claude Code exactly how to react to failures:

Run npm run check and npm run test.
If either fails, read only the first failing error, explain the cause, and make the smallest fix.
After the fix, summarize the git diff for src/lib and src/routes.
Do not commit.

Practical Use Cases

Use caseGood Claude Code workHuman review focus
Admin filtersSplit filter state into $state and computed views with $derivedWhich filters belong in the URL, and what permissions hide
Blog or CMS detail pagesWire [slug], load, SEO <svelte:head>, and 404 handlingSanitizing HTML, draft visibility, preview rules
Contact and lead formsBuild actions, validation, use:enhance, and testsPrivacy, spam handling, notification target
Svelte 4 to Svelte 5 migrationConvert selected components from export let and $: to runesAvoiding broad behavior changes during automated migration

The revenue path is straightforward: SvelteKit pages and forms can connect technical articles to consultation or product flows. On ClaudeCodeLab, a reader can move from this article to the Claude Code consultation page when they need rollout help. For more context, read the Claude Code getting started guide, TypeScript tips, and testing strategies.

Pitfalls to Avoid

The first pitfall is asking for too much at once. “Build a SvelteKit SaaS” mixes routes, auth, database design, UI, tests, billing, and deployment. “Add the /tasks list view without touching auth” is a much better prompt.

The second pitfall is mixing Svelte 4 and Svelte 5 styles. If a repo uses runes, tell Claude Code to keep $props, $state, and $derived. If a repo is still on legacy syntax, do not let the agent migrate the whole app unless migration is the explicit task.

The third pitfall is leaking server-only code. Do not import $lib/server modules from .svelte components. Keep secrets, database clients, and privileged APIs in server files.

The fourth pitfall is abusing $effect. The Svelte docs explain that effects run in the browser and can become hard to reason about if they update state. Prefer $derived for display calculations and reserve $effect for browser APIs and external libraries.

The fifth pitfall is accepting weak tests. A test that only checks “the button exists” will not catch a broken callback or validation flow. Test the click, the error message, the success state, and the 404 path where appropriate.

Official References and a Prompt Template

Use primary sources when behavior matters: Svelte docs, SvelteKit docs, SvelteKit form actions, Vite guide, and Claude Code docs. When prompting Claude Code, ask it to follow the current official docs rather than relying on old examples from memory.

Read the current SvelteKit structure, then change only this scope.
Files: src/routes/contact/+page.svelte and src/routes/contact/+page.server.ts
Goal: add a company field to the inquiry form.
Constraints:
- Keep Svelte 5 runes syntax.
- Do not remove use:enhance.
- Add server-side validation.
- Do not change existing CTA copy or layout classes.
- Run npm run check before finishing.
End with a 3-line summary: changes, risk, and missing tests.

Summary and Tested Result

Claude Code works well with Svelte and SvelteKit when you make the framework boundaries explicit. Use $props, $state, and $derived for components, keep server-only work in +page.server.ts or $lib/server, validate forms through actions, and make Claude run the same checks a teammate would run.

After trying this workflow in a small SvelteKit task app, Masa found that starting in plan mode reduced rework the most. Prompts that said “keep runes syntax,” “run npm run check,” and “do not commit” produced smaller diffs and made the final review much easier for a Svelte beginner.

#Claude Code #Svelte #SvelteKit #frontend #TypeScript
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.