SvelteKit is the official application framework for Svelte. While Svelte is the component framework, SvelteKit provides the full-stack layer: routing, server-side rendering, API routes, and deployment adapters. Together, they offer an incredibly productive development experience.
This guide covers everything you need to build production-ready applications with SvelteKit.
Why SvelteKit?
SvelteKit brings together the best of modern web development:
- Svelte’s reactivity — No virtual DOM, compiles to vanilla JavaScript
- File-based routing — Your folder structure is your routes
- Server-Side Rendering (SSR) — Great SEO and initial load performance
- API Routes — Backend and frontend in one project
- TypeScript — First-class support throughout
Project Setup
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
During setup, choose Skeleton project for a clean start, and enable TypeScript and ESLint.
File-Based Routing
SvelteKit’s routing is based on the filesystem:
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/[slug]
└── api/
└── users/
└── +server.ts → /api/users (API endpoint)
Special Route Files
| File | Purpose |
|---|---|
+page.svelte | Page component |
+page.ts | Page load function (runs on server + client) |
+page.server.ts | Server-only load function |
+layout.svelte | Shared layout wrapper |
+layout.ts | Layout load function |
+server.ts | API endpoint (GET, POST, etc.) |
+error.svelte | Error page |
Loading Data
The +page.ts (or +page.server.ts) file exports a load function that fetches data for the page:
// src/routes/blog/+page.ts
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch("/api/posts");
const posts = await response.json();
return {
posts, // Available as `data.posts` in the page
};
};
In the page component, use the data prop:
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<p>{post.description}</p>
</article>
{/each}
Server-Only Load Functions
Use +page.server.ts for database queries and sensitive operations:
// src/routes/dashboard/+page.server.ts
import { db } from "$lib/db";
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ locals }) => {
// Access session from locals (set by auth hook)
if (!locals.user) {
redirect(302, "/login");
}
const stats = await db.query.analytics.findMany({
where: { userId: locals.user.id },
orderBy: { date: "desc" },
limit: 30,
});
return { stats };
};
API Routes
SvelteKit lets you create API endpoints using +server.ts files:
// src/routes/api/posts/+server.ts
import { json, error } from "@sveltejs/kit";
import { db } from "$lib/db";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get("page") ?? "1");
const limit = 10;
const posts = await db.query.posts.findMany({
where: { published: true },
orderBy: { publishedAt: "desc" },
limit,
offset: (page - 1) * limit,
});
return json(posts);
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user?.isAdmin) {
error(403, "Forbidden");
}
const body = await request.json();
const post = await db
.insert(posts)
.values({
title: body.title,
content: body.content,
authorId: locals.user.id,
publishedAt: new Date(),
})
.returning();
return json(post[0], { status: 201 });
};
Forms and Actions
SvelteKit’s Form Actions provide a progressive enhancement-friendly way to handle form submissions:
// src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { sendEmail } from "$lib/email";
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get("email") as string;
const message = data.get("message") as string;
if (!email?.includes("@")) {
return fail(400, {
email,
message,
error: "Please provide a valid email address",
});
}
await sendEmail({ to: "hello@kingtechnologies.dev", from: email, message });
return { success: true };
},
};
In the page, the form works without JavaScript (progressive enhancement):
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<form method="POST" use:enhance>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
{#if form?.success}
<p class="success">Message sent!</p>
{/if}
<label>
Email
<input type="email" name="email" value={form?.email ?? ''} required />
</label>
<label>
Message
<textarea name="message" required>{form?.message ?? ''}</textarea>
</label>
<button type="submit">Send</button>
</form>
Layouts and Nested Routing
Layouts wrap pages and persist across navigation:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
let { children } = $props();
</script>
<Header />
<main>
{@render children()}
</main>
<Footer />
Nested Layouts
src/routes/
├── +layout.svelte # Root layout (header + footer)
├── blog/
│ ├── +layout.svelte # Blog layout (sidebar + main)
│ ├── +page.svelte # /blog — wrapped by BOTH layouts
│ └── [slug]/
│ └── +page.svelte # /blog/[slug] — also wrapped by both
└── dashboard/
├── +layout.svelte # Dashboard layout (sidebar nav)
└── +page.svelte # /dashboard
Hooks: The Middleware Layer
hooks.server.ts runs on every server request — perfect for authentication:
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
import { verifyToken } from "$lib/auth";
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get("auth-token");
if (token) {
const user = await verifyToken(token);
event.locals.user = user ?? null;
}
const response = await resolve(event);
// Add security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
};
Stores and State Management
Svelte’s reactive stores work naturally with SvelteKit:
// src/lib/stores/cart.ts
import { writable, derived } from "svelte/store";
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
const createCartStore = () => {
const { subscribe, update, set } = writable<CartItem[]>([]);
return {
subscribe,
addItem: (item: Omit<CartItem, "quantity">) =>
update((items) => {
const existing = items.find((i) => i.id === item.id);
if (existing) {
return items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i,
);
}
return [...items, { ...item, quantity: 1 }];
}),
removeItem: (id: string) =>
update((items) => items.filter((i) => i.id !== id)),
clear: () => set([]),
};
};
export const cart = createCartStore();
export const cartTotal = derived(cart, ($cart) =>
$cart.reduce((total, item) => total + item.price * item.quantity, 0),
);
export const cartCount = derived(cart, ($cart) =>
$cart.reduce((count, item) => count + item.quantity, 0),
);
Error Handling
SvelteKit has excellent built-in error handling:
// For expected errors (shown to user)
import { error } from "@sveltejs/kit";
error(404, "Post not found");
// For unexpected errors (generic 500 page)
throw new Error("Database connection failed");
Create a custom error page:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message}</h1>
{#if $page.status === 404}
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go home</a>
{/if}
Deployment
SvelteKit adapts to your deployment target:
# Node.js server
npm install @sveltejs/adapter-node
# Vercel (auto-detected)
npm install @sveltejs/adapter-vercel
# Static site generation
npm install @sveltejs/adapter-static
# Cloudflare Pages/Workers
npm install @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from "@sveltejs/adapter-vercel";
export default {
kit: {
adapter: adapter({
runtime: "nodejs22.x",
}),
},
};
Performance Tips
- Preload routes with
data-sveltekit-preload-data:
<a href="/blog" data-sveltekit-preload-data="hover">Blog</a>
- Stream slow data to avoid blocking the initial render:
// +page.server.ts
export const load: PageServerLoad = async () => {
return {
// Fast data loads immediately
user: await getUser(),
// Slow data streams in — page renders before this resolves
analytics: getAnalytics(), // No await!
};
};
- Use
$app/storesfor accessing page info without prop drilling:
<script>
import { page } from '$app/stores';
</script>
<!-- Active link based on current URL -->
<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>
Conclusion
SvelteKit is a complete framework for building modern web applications. Its file-based routing, built-in SSR, form actions, and TypeScript support make it incredibly productive once you understand the conventions.
The framework scales from simple static sites to complex full-stack applications with databases, authentication, and real-time features. Combined with Svelte’s minimal JavaScript output, the result is applications that are fast by default.
Start with a simple project, learn the routing and data loading patterns, then gradually add server-side features as needed. SvelteKit’s progressive nature means you only add complexity when you actually need it.