Imported from sickn33/agentic-awesome-skills (
skills/sveltekit/SKILL.md). Install upstream withnpx skills add sickn33/agentic-awesome-skills --skill sveltekit. Copyright stays with the author.
SvelteKit Full-Stack Development
Overview
SvelteKit is the official full-stack framework built on top of Svelte. It provides file-based routing, server-side rendering (SSR), static site generation (SSG), API routes, and progressive form actions — all with Svelte's compile-time reactivity model that reduces some runtime work through compilation; client-side features still ship runtime JavaScript. Use this skill when building fast, modern web apps where both DX and performance matter.
When to Use This Skill
- Use when building a new full-stack web application with Svelte
- Use when you need SSR or SSG with fine-grained control per route
- Use when migrating a SPA to a framework with server capabilities
- Use when working on a project that needs file-based routing and collocated API endpoints
- Use when the user asks about
+page.svelte,+layout.svelte,loadfunctions, or form actions
Version and integration scope
Examples use legacy Svelte component syntax and omit DB/email/auth implementations. Use the scaffold and docs matching installed Svelte/SvelteKit versions. Validate/allowlist POST fields and authorize mutations before DB writes; data: body is not a production mass-assignment pattern. Handle failed fetch status before parsing JSON, and do not treat built-in form origin checks as complete authorization.
How It Works
Step 1: Project Setup
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
Choose Skeleton project + TypeScript + ESLint/Prettier when prompted.
Directory structure after scaffolding:
src/
routes/
+page.svelte ← Root page component
+layout.svelte ← Root layout (wraps all pages)
+error.svelte ← Error boundary
lib/
server/ ← Server-only code (never bundled to client)
components/ ← Shared components
app.html ← HTML shell
static/ ← Static assets
Step 2: File-Based Routing
Every +page.svelte file in src/routes/ maps directly to a URL:
src/routes/+page.svelte → /
src/routes/about/+page.svelte → /about
src/routes/blog/[slug]/+page.svelte → /blog/:slug
src/routes/shop/[...path]/+page.svelte → /shop/* (catch-all)
Route groups (no URL segment): wrap in (group)/ folder.
Route groups are not private routes. Parentheses affect URL/layout organization, not authorization. Guard sensitive loads, actions and endpoints on the server.
Step 3: Loading Data with load Functions
Use a +page.ts (universal) or +page.server.ts (server-only) file alongside the page:
// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch }) => {
const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json());
if (!post) {
error(404, 'Post not found');
}
return { post };
};
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>{data.post.title}</h1>
<article>{data.post.content}</article> <!-- Escaped text; use @html only for trusted/sanitized HTML. -->
Step 4: API Routes (Server Endpoints)
Create +server.ts files for REST-style endpoints:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit') ?? 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
return json({ error: "Invalid limit" }, { status: 400 });
}
const posts = await db.post.findMany({ take: limit });
return json(posts);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
const post = await db.post.create({ data: body });
return json(post, { status: 201 });
};
Step 5: Form Actions
Form actions are the SvelteKit-native way to handle mutations — no client-side fetch required:
// src/routes/contact/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) {
return fail(400, { email, missing: true });
}
await sendEmail(String(email));
redirect(303, '/thank-you');
}
};
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<form method="POST" use:enhance>
<input name="email" type="email" />
{#if form?.missing}<p class="error">Email is required</p>{/if}
<button type="submit">Subscribe</button>
</form>
Step 6: Layouts and Nested Routes
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
</script>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
{#if data.user}
<a href="/dashboard">Dashboard</a>
{/if}
</nav>
<slot /> <!-- child page renders here -->
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return { user: locals.user ?? null };
};
Step 7: Rendering Modes
Control per-route rendering with page options:
// src/routes/docs/+page.ts
export const prerender = true; // Static — generated at build time
export const ssr = true; // Default — rendered on server per request
export const csr = false; // Disable client-side hydration entirely
Examples
Example 1: Dashboard Redirect (not complete authorization)
A layout redirect is useful navigation behavior but is not a substitute for authorization at every protected server data/action/endpoint boundary. Return only public user fields from server loads; their data is serialized to the client. See SvelteKit auth, route groups and raw HTML safety.
// src/routes/dashboard/+layout.server.ts
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
redirect(303, '/login');
}
return { user: locals.user };
};
Example 2: Hooks — Session Middleware
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { verifyToken } from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (token) {
event.locals.user = await verifyToken(token);
}
return resolve(event);
};
Example 3: Preloading and Invalidation
<script lang="ts">
import { invalidateAll } from '$app/navigation';
async function refresh() {
await invalidateAll(); // re-runs all load functions on the page
}
</script>
<button on:click={refresh}>Refresh</button>
Best Practices
- ✅ Use
+page.server.tsfor database/auth logic — it never ships to the client - ✅ Use
$lib/server/for shared server-only modules (DB client, auth helpers) - ✅ Use form actions for mutations instead of client-side
fetch— works without JS - ✅ Type all
loadreturn values with generated$types(PageData,LayoutData) - ✅ Use
event.localsin hooks to pass server-side context to load functions - ❌ Don't import server-only code in
+page.svelteor+layout.sveltedirectly - ❌ Don't store sensitive state in stores — use
localson the server - ❌ Don't skip
use:enhanceon forms — without it, forms lose progressive enhancement
Security & Safety Notes
- All code in
+page.server.ts,+server.ts, and$lib/server/runs exclusively on the server — safe for DB queries, secrets, and session validation. - Always validate and sanitize form data before database writes.
- Use
error(403)orredirect(303)from@sveltejs/kitrather than returning raw error objects. - Set
httpOnly: trueandsecure: trueon all auth cookies. - CSRF protection is built-in for form actions — do not disable
checkOriginin production.
Common Pitfalls
-
Problem:
Cannot use import statement in a modulein+page.server.tsSolution: The file must be.tsor.js, not.svelte. Server files and Svelte components are separate. -
Problem: Store value is
undefinedon first SSR render Solution: Populate the store from theloadfunction return value (dataprop), not from client-sideonMount. -
Problem: Form action does not redirect after submit Solution: Use
redirect(303, '/path')from@sveltejs/kit, not a plainreturn. 303 is required for POST redirects. -
Problem:
locals.useris undefined inside a+page.server.tsload function Solution: Setevent.locals.userinsrc/hooks.server.tsbefore theresolve()call.
Related Skills
@nextjs-app-router-patterns— When you prefer React over Svelte for SSR/SSG@trpc-fullstack— Add end-to-end type safety to SvelteKit API routes@auth-implementation-patterns— Authentication patterns usable with SvelteKit hooks@tailwind-patterns— Styling SvelteKit apps with Tailwind CSS
Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
