SvelteKit: Building Full-Stack Svelte Apps
SvelteKit is Svelte's official full-stack framework — it takes Svelte's compile-time reactivity and layers on everything a production application needs: file-based routing, server-side rendering, server-only data loading, API endpoints, and a flexible adapter system that deploys anywhere from Vercel to a bare Node.js server. Think of it as Next.js for React or Nuxt for Vue, but built on a fundamentally different philosophy.
Where React and Vue carry a runtime library into the browser, Svelte compiles your components away at build time. The result is smaller bundles, faster Time to Interactive, and components that read closer to plain HTML and JavaScript. SvelteKit adds the routing and server layers on top of that, using Vite as the build tool for near-instant hot module replacement during development.
Released as stable in December 2022, SvelteKit has been battle-tested in production by teams who prioritize performance and developer experience. In this guide you will build a solid understanding of how SvelteKit works — from file-based routing and load functions through to API routes, form actions, and deployment adapters.
Svelte compiles components to optimized vanilla JavaScript — no virtual DOM reconciliation at runtime. SvelteKit pages typically ship 30–50% less JavaScript than equivalent Next.js pages. Server-side rendering is built in by default, so users see content before any JavaScript executes, and navigation between pages uses client-side routing with prefetching for near-instant transitions.
What is SvelteKit?
SvelteKit is the result of layering a full web framework on top of the Svelte compiler. Svelte alone gives you reactive components; SvelteKit gives you the rest of what you need to ship an application:
- File-based routing: every file in
src/routesmaps to a URL — no configuration needed - Server-side rendering (SSR): pages are rendered on the server by default for fast first-paint and SEO
- Static site generation (SSG): pre-render pages at build time with the static adapter
- Server-only load functions: fetch data from databases and call secrets-protected APIs before the page renders
- API routes: build backend endpoints alongside your frontend without a separate server
- Form actions: handle form submissions server-side, with progressive enhancement so forms work without JavaScript
- Adapter system: one codebase deploys to Node.js, Vercel, Netlify, Cloudflare Workers, and more
- TypeScript first: TypeScript is configured automatically during project creation
| Feature | SvelteKit | Next.js | Nuxt |
|---|---|---|---|
| Base framework | Svelte | React | Vue 3 |
| Build tool | Vite | Turbopack / Webpack | Vite / Nitro |
| Bundle approach | Compiled (no runtime) | Runtime (React ~45 KB) | Runtime (Vue ~35 KB) |
| SSR default | Yes | Yes (App Router) | Yes |
| Data loading | +page.server.js | Server Components | useFetch / useAsyncData |
| API routes | +server.js | app/api/route.js | server/api/ |
| TypeScript support | First-class | First-class | First-class |
Installation and Project Setup
SvelteKit projects are scaffolded with npm create svelte@latest, which walks you through a small interactive setup wizard. You only need Node.js 18 or later.
Scaffold a new project
Run the create command, choose a template, and select TypeScript support and any optional tooling (ESLint, Prettier, Playwright, Vitest).
npm create svelte@latest my-sveltekit-app
# Interactive prompts:
# ✔ Which Svelte app template? › Skeleton project
# ✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
# ✔ Select additional options: › ESLint, Prettier, Vitest
cd my-sveltekit-app
npm install
npm run dev
Explore the project structure
All routing lives in src/routes. Shared utilities and components go in src/lib and are importable via the $lib alias.
my-sveltekit-app/
├── src/
│ ├── routes/
│ │ ├── +layout.svelte # Root layout (nav, footer)
│ │ ├── +page.svelte # Home page → /
│ │ ├── blog/
│ │ │ ├── +page.svelte # Blog list → /blog
│ │ │ └── [slug]/
│ │ │ ├── +page.svelte # Blog post → /blog/:slug
│ │ │ └── +page.server.ts # Server-side data loading
│ │ └── api/
│ │ └── posts/
│ │ └── +server.ts # API route → /api/posts
│ ├── lib/
│ │ ├── components/ # Shared Svelte components
│ │ └── server/ # Server-only utilities (DB, etc.)
│ └── app.html # HTML shell template
├── static/ # Static assets (images, fonts)
├── svelte.config.js # SvelteKit configuration
└── vite.config.ts # Vite configuration
Configure your environment
Create a .env file for secrets. Variables prefixed with PUBLIC_ are exposed to the browser; all others remain server-only and are accessible via $env/static/private.
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
JWT_SECRET="your-secret-key-never-expose-this"
PUBLIC_API_BASE="https://api.myapp.com"
File-Based Routing
SvelteKit's router is entirely driven by the file system. Every directory inside src/routes is a route segment, and the contents of that directory determine what happens when a user visits that URL. The naming conventions use a + prefix to distinguish route files from regular components:
| File name | Purpose | Runs on |
|---|---|---|
+page.svelte |
Page UI component | Client + server (SSR) |
+layout.svelte |
Shared wrapper around child routes | Client + server (SSR) |
+page.server.ts |
Server-only load function & form actions | Server only |
+page.ts |
Universal load function | Server (SSR) + client (navigation) |
+server.ts |
REST API endpoint | Server only |
+error.svelte |
Custom error page | Client + server (SSR) |
Dynamic Routes and Route Groups
Wrap a segment in square brackets to capture a dynamic value: src/routes/blog/[slug] matches /blog/any-slug-here. Use spread syntax for catch-all routes: [...path]. Wrap a segment in parentheses to create a route group that shares a layout without affecting the URL: (marketing), (app).
SvelteKit routes map directly to the file system in src/routes/
Here is a root layout that wraps every page with navigation, and a simple home page that uses the data prop passed from a load function:
<script lang="ts">
import { page } from '$app/stores';
</script>
<nav>
<a href="/" class:active={$page.url.pathname === '/'}>Home</a>
<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>
<a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
</nav>
<main>
<slot />
</main>
<footer>
<p>© 2026 My SvelteKit App</p>
</footer>
<style>
nav a.active { color: #FF3E00; font-weight: 600; }
</style>
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let count = 0;
const increment = () => count++;
</script>
<svelte:head>
<title>{data.siteTitle}</title>
</svelte:head>
<h1>Welcome to {data.siteTitle}</h1>
<p>Posts published: {data.postCount}</p>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
Loading Data with Load Functions
SvelteKit separates data fetching from rendering through load functions. A load function runs before its page renders, fetches whatever the page needs, and returns data that becomes the data prop on +page.svelte. There are two variants:
- +page.server.ts — runs exclusively on the server. Can import database clients, read environment variables, and call any Node.js API. Data is serialized and sent to the client as JSON.
- +page.ts — a "universal" load that runs on the server during SSR and re-runs in the browser during client-side navigation. Use it when you only need
fetchand don't need server-only resources.
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug },
include: { author: true, tags: true }
});
if (!post) {
// SvelteKit renders +error.svelte with this message
throw error(404, `Post "${params.slug}" not found`);
}
return {
post,
// Only serializable data reaches the client
meta: {
title: post.title,
description: post.excerpt
}
};
};
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const { post, meta } = data;
</script>
<svelte:head>
<title>{meta.title}</title>
<meta name="description" content={meta.description}>
</svelte:head>
<article>
<h1>{post.title}</h1>
<p class="byline">
By {post.author.name} •
{new Date(post.publishedAt).toLocaleDateString()}
</p>
<div class="content">{@html post.htmlContent}</div>
<ul class="tags">
{#each post.tags as tag}
<li><a href="/blog?tag={tag.slug}">{tag.name}</a></li>
{/each}
</ul>
</article>
Caching and Revalidation
Load functions can set HTTP cache headers so that CDNs cache rendered pages. Return a setHeaders call from your load function to control the Cache-Control header, or use SvelteKit's built-in invalidate() and invalidateAll() on the client to force a reload when data changes.
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ setHeaders }) => {
const posts = await db.post.findMany({
where: { published: true },
orderBy: { publishedAt: 'desc' },
select: { id: true, title: true, slug: true, excerpt: true, publishedAt: true }
});
// Cache the page at the CDN for 60 seconds, allow stale for 300 s
setHeaders({
'Cache-Control': 'public, max-age=60, stale-while-revalidate=300'
});
return { posts };
};
Because +page.server.ts load functions run exclusively on the server, window, document, localStorage, and sessionStorage do not exist. For universal load functions (+page.ts) that run on both, guard browser-only calls with the browser boolean from $app/environment: if (browser) { ... }.
API Routes and Form Actions
SvelteKit gives you two ways to handle server-side mutations: API routes with +server.ts files that respond to HTTP requests programmatically, and form actions in +page.server.ts that handle HTML form submissions with progressive enhancement.
API Routes with +server.ts
Export named functions matching HTTP verbs. SvelteKit's json() helper sets the correct Content-Type and serializes the response:
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { RequestHandler } from './$types';
// GET /api/posts?page=1&limit=10
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page') ?? '1');
const limit = Number(url.searchParams.get('limit') ?? '10');
const [posts, total] = await Promise.all([
db.post.findMany({
where: { published: true },
skip: (page - 1) * limit,
take: limit,
orderBy: { publishedAt: 'desc' }
}),
db.post.count({ where: { published: true } })
]);
return json({ posts, total, page, pages: Math.ceil(total / limit) });
};
// POST /api/posts
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user?.isAdmin) throw error(403, 'Forbidden');
const body = await request.json();
if (!body.title || !body.content) {
throw error(400, 'title and content are required');
}
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
slug: body.title.toLowerCase().replace(/\s+/g, '-'),
authorId: locals.user.id
}
});
return json(post, { status: 201 });
};
Form Actions — Progressive Enhancement
Form actions are a more ergonomic alternative for operations triggered by forms. They work without JavaScript (full page reload) and upgrade transparently to client-side submissions with SvelteKit's enhance action:
import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { Actions } from './$types';
export const actions: Actions = {
// matches action="?/create" on the form
create: async ({ request, locals }) => {
if (!locals.user) throw redirect(303, '/login');
const data = await request.formData();
const title = data.get('title')?.toString().trim();
const content = data.get('content')?.toString().trim();
if (!title || title.length < 3) {
return fail(400, {
error: 'Title must be at least 3 characters',
fields: { title, content }
});
}
await db.post.create({
data: { title, content, authorId: locals.user.id }
});
throw redirect(303, '/blog');
}
};
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<!-- use:enhance upgrades to fetch-based submission -->
<form method="POST" action="?/create" use:enhance>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<label>
Title
<input
name="title"
value={form?.fields?.title ?? ''}
required minlength="3"
>
</label>
<label>
Content
<textarea name="content">{form?.fields?.content ?? ''}</textarea>
</label>
<button type="submit">Publish</button>
</form>
A page request: route matched, server load called, page rendered, HTML returned, then hydrated in the browser
Deploying SvelteKit with Adapters
SvelteKit's adapter system is what makes the same codebase deployable to radically different environments. An adapter transforms the SvelteKit build output into whatever format the target platform expects. Install the adapter and configure it in svelte.config.js.
adapter-auto detects your deployment platform automatically (Vercel, Netlify, Cloudflare, etc.) and applies the correct adapter. Best for zero-config deployments.
npm install -D @sveltejs/adapter-auto
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
export default {
kit: { adapter: adapter() }
};
adapter-node produces a self-contained Node.js server. Great for Docker containers, VPS deployments, and anywhere you run your own infrastructure.
npm install -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({
out: 'build', // output directory
precompress: true // gzip + brotli static assets
})
}
};
// Build and run:
// npm run build
// node build/index.js
adapter-vercel deploys pages as serverless or edge functions. Each route can opt into the edge runtime for global low-latency execution.
npm install -D @sveltejs/adapter-vercel
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
export default {
kit: {
adapter: adapter({
runtime: 'edge' // or 'nodejs20.x'
})
}
};
// Per-route override in +page.server.ts:
export const config = { runtime: 'nodejs20.x' };
adapter-static pre-renders your entire site to static HTML at build time. Perfect for blogs, documentation, and marketing sites with no runtime server needed.
npm install -D @sveltejs/adapter-static
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html' // for SPAs with client-side routing
}),
prerender: {
entries: ['*'] // pre-render all discovered pages
}
}
};
Building and Previewing
# Development server with HMR
npm run dev
# Type-check without building
npm run check
# Production build
npm run build
# Preview the production build locally (uses adapter-node or adapter-auto)
npm run preview
Key Takeaways and Next Steps
SvelteKit gives you a coherent full-stack framework without the framework fatigue that comes with assembling a React stack from scratch. The file-system conventions are opinionated enough to eliminate decision paralysis, but flexible enough to handle real-world complexity. Here is a summary of the core ideas:
- Routes are files. Every folder in
src/routesis a URL segment; the+-prefixed files define the page, layout, data, and API behavior for that segment. - Data and UI are decoupled by design. Load functions run before the page component, keep secrets server-side, and return plain serializable data — the page receives it as a typed
dataprop. - Server and client share the same codebase. You write one app; SvelteKit routes server and client code to the right place at build time.
- Form actions work without JavaScript. SvelteKit embraces the web platform: forms that progressively enhance are more resilient and accessible than JavaScript-only mutations.
- Adapters make deployment trivial. Swap one package and one line in the config to move from a Node server to Vercel edge functions to a static CDN.
- Svelte's reactivity stays simple. No hooks rules, no dependency arrays, no context/provider boilerplate — reactive declarations with
$:and Svelte stores cover most use cases cleanly.
What to Explore Next
- Svelte Stores:
writable,readable, andderivedstores for shared reactive state across components - Hooks:
src/hooks.server.tsfor authentication middleware, database client injection intolocals, and request logging - Authentication: Lucia Auth or Auth.js (NextAuth for SvelteKit) for production-grade session management
- Database with Prisma: Pair SvelteKit with Prisma ORM for type-safe database queries that complement SvelteKit's generated types
- Testing: Vitest for unit tests, Playwright for end-to-end tests — both configured automatically by the project wizard
- Streaming: SvelteKit supports streaming SSR, letting you send a shell immediately and stream slow data as it resolves
"SvelteKit is what happens when you design a framework for the web platform rather than around a runtime abstraction. Less framework, more web."
— Rich Harris, Creator of Svelte
SvelteKit's strength is in how little it asks you to learn that isn't already the web. HTTP verbs, form submissions, URL search params, cache headers — these are all first-class concepts in SvelteKit rather than things you work around. That alignment with the platform is why SvelteKit apps tend to be faster, smaller, and easier to reason about than their peers.