CtrlK
BlogDocsLog inGet started
Tessl Logo

svelte-project-starter

Scaffold a SvelteKit 2.x project with Svelte 5 runes, file-based routing, load functions, form actions, and adapter configuration for multiple deployment targets.

77

Quality

72%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./frontend/svelte-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Svelte Project Starter

Scaffold a SvelteKit 2.x project with Svelte 5 runes, file-based routing, load functions, form actions, and adapter configuration for multiple deployment targets.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x (or pnpm >= 9.x)
  • Git

Scaffold Command

npx sv create my-app
# Select: SvelteKit minimal, TypeScript, ESLint, Prettier, Tailwind CSS
cd my-app
npm install

The sv create CLI will prompt for options. Select:

  • Template: SvelteKit minimal
  • TypeScript: Yes
  • Add-ons: ESLint, Prettier, Tailwind CSS (via @tailwindcss/vite)

Project Structure

src/
├── routes/
│   ├── +layout.svelte         # Root layout (wraps all pages)
│   ├── +layout.server.ts      # Root layout server load function
│   ├── +page.svelte           # Home page (/)
│   ├── +page.server.ts        # Home page server load + form actions
│   ├── +error.svelte          # Error page
│   ├── login/
│   │   ├── +page.svelte       # /login
│   │   └── +page.server.ts    # Login form action
│   ├── dashboard/
│   │   ├── +page.svelte       # /dashboard
│   │   ├── +page.server.ts    # Dashboard data loader
│   │   └── +layout.svelte     # Dashboard layout (sidebar)
│   ├── users/
│   │   ├── +page.svelte       # /users
│   │   ├── +page.server.ts    # Users list loader
│   │   └── [id]/
│   │       ├── +page.svelte   # /users/:id
│   │       └── +page.server.ts
│   └── api/
│       └── health/
│           └── +server.ts     # GET /api/health (API route)
├── lib/
│   ├── components/            # Reusable components
│   │   ├── ui/                # UI primitives (Button, Card, etc.)
│   │   └── layout/            # Layout components (Header, Sidebar)
│   ├── stores/                # Svelte stores (writable, derived)
│   ├── services/              # API client functions
│   ├── utils/                 # Pure utility functions
│   └── types/                 # Shared TypeScript types
├── hooks.server.ts            # Server hooks (handle, handleFetch, handleError)
├── app.html                   # HTML template
├── app.css                    # Global CSS (Tailwind entry)
└── app.d.ts                   # App-level type declarations
static/                        # Static assets served at /
svelte.config.js               # SvelteKit configuration
vite.config.ts                 # Vite configuration
.env.example                   # Required env vars template

Key Conventions

  • Svelte 5 runes: use $state, $derived, $effect, $props, and $bindable instead of legacy reactive declarations ($:, let, stores for local state).
  • File-based routing: +page.svelte files define pages. +layout.svelte files wrap child routes. +server.ts files define API endpoints.
  • Load functions: data fetching happens in +page.server.ts (server-only) or +page.ts (universal). Data is passed to the page via the data prop.
  • Form actions: mutations use HTML <form> elements with server-side actions defined in +page.server.ts. Progressive enhancement built-in.
  • $lib alias: src/lib/ is aliased to $lib for clean imports.
  • Server hooks: hooks.server.ts runs middleware-like logic (auth, logging) on every server request.
  • Adapters: SvelteKit builds for different targets via adapters (Node, Vercel, Cloudflare, static).

Essential Patterns

SvelteKit Config (svelte.config.js)

import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter(),
    alias: {
      $components: "src/lib/components",
    },
  },
};

export default config;

Vite Config (vite.config.ts)

import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tailwindcss(), sveltekit()],
});

Tailwind CSS Entry (src/app.css)

@import "tailwindcss";

Root Layout (src/routes/+layout.svelte)

<script lang="ts">
  import "../app.css";
  import Header from "$lib/components/layout/Header.svelte";

  let { children, data } = $props();
</script>

<div class="min-h-screen bg-gray-50">
  <Header user={data.user} />
  <main class="p-6">
    {@render children()}
  </main>
</div>

Root Layout Server Load (src/routes/+layout.server.ts)

import type { LayoutServerLoad } from "./$types";

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user ?? null,
  };
};

Page with Server Load Function

<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";
  import UserCard from "$lib/components/ui/UserCard.svelte";

  let { data }: { data: PageData } = $props();

  let searchQuery = $state("");

  let filteredUsers = $derived(
    data.users.filter((u) =>
      u.name.toLowerCase().includes(searchQuery.toLowerCase())
    )
  );
</script>

<h1 class="mb-4 text-2xl font-bold">Dashboard</h1>

<input
  type="text"
  bind:value={searchQuery}
  placeholder="Search users..."
  class="mb-4 rounded border px-3 py-2"
/>

<p class="mb-2 text-sm text-gray-500">
  Showing {filteredUsers.length} of {data.users.length} users
</p>

{#each filteredUsers as user (user.id)}
  <UserCard {user} />
{:else}
  <p class="text-gray-500">No users found.</p>
{/each}

Server Load Function

// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types";
import { error, redirect } from "@sveltejs/kit";

export const load: PageServerLoad = async ({ locals, fetch }) => {
  if (!locals.user) {
    redirect(302, "/login");
  }

  const response = await fetch("/api/users");
  if (!response.ok) {
    error(500, "Failed to load users");
  }

  const users = await response.json();

  return { users };
};

Component with Svelte 5 Runes

<!-- src/lib/components/ui/UserCard.svelte -->
<script lang="ts">
  interface User {
    id: string;
    name: string;
    email: string;
  }

  interface Props {
    user: User;
    selected?: boolean;
    onselect?: (id: string) => void;
    ondelete?: (id: string) => void;
  }

  let { user, selected = false, onselect, ondelete }: Props = $props();
</script>

<div
  class="mb-2 rounded border p-3 cursor-pointer"
  class:border-blue-500={selected}
  class:bg-blue-50={selected}
  onclick={() => onselect?.(user.id)}
  role="button"
  tabindex="0"
>
  <p class="font-medium">{user.name}</p>
  <p class="text-sm text-gray-500">{user.email}</p>
  {#if ondelete}
    <button
      class="mt-2 text-sm text-red-600 hover:underline"
      onclick={(e) => { e.stopPropagation(); ondelete?.(user.id); }}
    >
      Delete
    </button>
  {/if}
</div>

Reactive State with Runes

<script lang="ts">
  // $state — reactive state (replaces `let x = ...` reactivity)
  let count = $state(0);

  // $derived — computed values (replaces `$: derived = ...`)
  let doubled = $derived(count * 2);
  let message = $derived(count === 0 ? "Click to start" : `Count: ${count}`);

  // $effect — side effects (replaces `$: { ... }` blocks)
  $effect(() => {
    document.title = `Count: ${count}`;
  });

  // $effect with cleanup
  $effect(() => {
    const interval = setInterval(() => {
      count += 1;
    }, 1000);

    return () => clearInterval(interval);
  });
</script>

<button onclick={() => count++} class="rounded bg-blue-600 px-4 py-2 text-white">
  {message} (doubled: {doubled})
</button>

Form Actions (Mutations)

<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { ActionData } from "./$types";

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

<h1 class="mb-4 text-2xl font-bold">Sign In</h1>

<form method="POST" use:enhance class="flex flex-col gap-4">
  <input
    name="email"
    type="email"
    placeholder="Email"
    class="rounded border px-3 py-2"
    required
  />
  <input
    name="password"
    type="password"
    placeholder="Password"
    class="rounded border px-3 py-2"
    required
  />

  {#if form?.error}
    <p class="text-sm text-red-600">{form.error}</p>
  {/if}

  <button type="submit" class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
    Sign In
  </button>
</form>
// src/routes/login/+page.server.ts
import type { Actions } from "./$types";
import { fail, redirect } from "@sveltejs/kit";

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const formData = await request.formData();
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    if (!email || !password) {
      return fail(400, { error: "Email and password are required", email });
    }

    // Authenticate user
    const result = await authenticateUser(email, password);

    if (!result.success) {
      return fail(401, { error: "Invalid credentials", email });
    }

    // Set session cookie
    cookies.set("session", result.token, {
      path: "/",
      httpOnly: true,
      sameSite: "lax",
      secure: true,
      maxAge: 60 * 60 * 24 * 7, // 1 week
    });

    redirect(302, "/dashboard");
  },
};

API Route (+server.ts)

// src/routes/api/health/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async () => {
  return json({ status: "ok", timestamp: new Date().toISOString() });
};
// src/routes/api/users/+server.ts
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ locals }) => {
  if (!locals.user) {
    error(401, "Unauthorized");
  }

  const users = await getUsers();
  return json(users);
};

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    error(401, "Unauthorized");
  }

  const body = await request.json();
  const user = await createUser(body);
  return json(user, { status: 201 });
};

Server Hooks (Middleware)

// src/hooks.server.ts
import type { Handle, HandleServerError } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
  // Read session from cookie
  const sessionToken = event.cookies.get("session");

  if (sessionToken) {
    // Validate token and set user on locals
    const user = await validateSession(sessionToken);
    event.locals.user = user;
  }

  const response = await resolve(event);
  return response;
};

export const handleError: HandleServerError = async ({ error, event }) => {
  console.error("Unhandled error:", error);
  return {
    message: "An unexpected error occurred",
  };
};

App-Level Type Declarations

// src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      user: {
        id: string;
        email: string;
        name: string;
      } | null;
    }

    interface Error {
      message: string;
    }

    interface PageData {
      user: App.Locals["user"];
    }
  }
}

export {};

Error Page

<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from "$app/state";
</script>

<div class="flex min-h-screen flex-col items-center justify-center gap-4">
  <h1 class="text-4xl font-bold">{page.status}</h1>
  <p class="text-gray-600">{page.error?.message ?? "An error occurred"}</p>
  <a href="/" class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
    Go Home
  </a>
</div>

Adapter Configuration

// svelte.config.js — Node.js adapter (self-hosted)
import adapter from "@sveltejs/adapter-node";

// svelte.config.js — Vercel adapter
import adapter from "@sveltejs/adapter-vercel";

// svelte.config.js — Static adapter (SSG)
import adapter from "@sveltejs/adapter-static";

// Install the adapter you need:
// npm install -D @sveltejs/adapter-node
// npm install -D @sveltejs/adapter-vercel
// npm install -D @sveltejs/adapter-static
// npm install -D @sveltejs/adapter-cloudflare

For static adapter, add to +layout.ts:

// src/routes/+layout.ts
export const prerender = true; // Prerender all pages

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: npm install
  3. Start dev server: npm run dev
  4. Verify the app loads at http://localhost:5173
  5. Run npm run check to confirm TypeScript and Svelte diagnostics pass

Common Commands

# Development
npm run dev                    # Start dev server (http://localhost:5173)

# Build
npm run build                  # Production build (uses configured adapter)
npm run preview                # Preview production build locally

# Lint & Format
npm run lint                   # Run ESLint + Prettier check
npm run format                 # Auto-format with Prettier

# Type check
npm run check                  # Run svelte-check (TypeScript + Svelte diagnostics)
npm run check:watch            # Watch mode

Integration Notes

  • Testing: use Vitest for unit tests (npm install -D vitest) and Playwright for E2E (npm install -D @playwright/test). SvelteKit scaffolds Playwright config by default.
  • Database: call DB directly in +page.server.ts load functions and form actions. These run server-side only. Pair with Prisma or Drizzle ORM.
  • Auth: the server hooks + cookies pattern shown above is the standard approach. Pair with Lucia auth library or implement custom JWT/session auth.
  • State management: Svelte 5 runes ($state, $derived) handle component state. For cross-component state, use Svelte stores (writable, derived) or pass data through load functions.
  • UI components: Tailwind handles styling. For prebuilt components, use shadcn-svelte or Skeleton UI.
  • Forms: SvelteKit form actions provide built-in progressive enhancement. For complex validation, pair with Zod and superforms (npm install sveltekit-superforms zod).
  • Deployment: choose the adapter for your target. adapter-auto detects the platform automatically on Vercel, Netlify, and Cloudflare.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.