SvelteKit is Svelte’s official application framework, similar to what Next.js is for React. It provides file-based routing, server-side rendering, API endpoints, and a deployment adapter system. This guide walks through building a full-stack CRUD application from scratch.
Project Setup
Scaffold a new SvelteKit project:
npx sv create my-appcd my-appnpm installnpm run devThe wizard lets you choose TypeScript, ESLint, Prettier, and testing setup. For this guide, we’ll use TypeScript.
The resulting project structure:
src/ routes/ +page.svelte # Home page +layout.svelte # Root layout lib/ index.ts # $lib alias entry app.html # HTML shellstatic/ # Static assetssvelte.config.js # SvelteKit configvite.config.ts # Vite configFile-Based Routing
Every directory under src/routes/ becomes a URL path. Each route can contain:
| File | Purpose |
|---|---|
+page.svelte | Page component (renders in browser) |
+page.ts | Universal load function (runs on server and client) |
+page.server.ts | Server-only load function and form actions |
+layout.svelte | Layout wrapping child routes |
+server.ts | API endpoint (GET, POST, PUT, DELETE) |
+error.svelte | Error boundary for this route |
For a task management app, the route structure might look like:
src/routes/ +layout.svelte +page.svelte # Dashboard tasks/ +page.svelte # Task list +page.server.ts # Load tasks, handle create/delete [id]/ +page.svelte # Single task view +page.server.ts # Load single task, handle update api/ tasks/ +server.ts # REST API endpointLoad Functions
Load functions fetch data before the page renders. They run on the server during SSR and on the client during navigation.
src/routes/tasks/+page.server.ts:
import type { PageServerLoad } from './$types';import { db } from '$lib/server/database';
export const load: PageServerLoad = async () => { const tasks = await db.task.findMany({ orderBy: { createdAt: 'desc' } });
return { tasks };};src/routes/tasks/+page.svelte:
<script lang="ts"> let { data } = $props();</script>
<h1>Tasks ({data.tasks.length})</h1>
<ul> {#each data.tasks as task} <li> <a href="/tasks/{task.id}">{task.title}</a> <span>{task.completed ? 'Done' : 'Pending'}</span> </li> {/each}</ul>The data prop is automatically typed based on what the load function returns. No manual type wiring needed.
Form Actions
Form actions handle form submissions server-side without writing client-side JavaScript. This is progressive enhancement by default.
src/routes/tasks/+page.server.ts:
import type { Actions } from './$types';import { fail } from '@sveltejs/kit';import { db } from '$lib/server/database';
export const actions: Actions = { create: async ({ request }) => { const formData = await request.formData(); const title = formData.get('title')?.toString();
if (!title || title.length < 1) { return fail(400, { title, missing: true }); }
await db.task.create({ data: { title, completed: false } });
return { success: true }; },
delete: async ({ request }) => { const formData = await request.formData(); const id = formData.get('id')?.toString();
if (!id) return fail(400, { message: 'Missing task ID' });
await db.task.delete({ where: { id } }); return { success: true }; }};Using form actions in the page:
<script lang="ts"> import { enhance } from '$app/forms'; let { data, form } = $props();</script>
<form method="POST" action="?/create" use:enhance> <input name="title" placeholder="New task..." /> {#if form?.missing} <p class="error">Title is required</p> {/if} <button type="submit">Add Task</button></form>
{#each data.tasks as task} <div> <span>{task.title}</span> <form method="POST" action="?/delete" use:enhance> <input type="hidden" name="id" value={task.id} /> <button type="submit">Delete</button> </form> </div>{/each}The use:enhance directive upgrades forms to use fetch instead of full-page navigation, while maintaining progressive enhancement. Without JavaScript, the forms still work via standard HTTP.
API Endpoints
For client-side fetching or external API consumers, create +server.ts files:
src/routes/api/tasks/+server.ts:
import { json } from '@sveltejs/kit';import type { RequestHandler } from './$types';import { db } from '$lib/server/database';
export const GET: RequestHandler = async () => { const tasks = await db.task.findMany(); return json(tasks);};
export const POST: RequestHandler = async ({ request }) => { const { title } = await request.json();
if (!title) { return json({ error: 'Title required' }, { status: 400 }); }
const task = await db.task.create({ data: { title, completed: false } });
return json(task, { status: 201 });};These endpoints respond to standard HTTP methods and can be consumed by any client.
Server-Only Code
Any code in $lib/server/ is guaranteed to never reach the client bundle. SvelteKit enforces this at build time. Use it for database connections, secrets, and server utilities:
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();Importing from $lib/server/ in a client-side file produces a build error, preventing accidental secret leakage.
Deployment
SvelteKit uses adapters for deployment targets:
# Node.js servernpm install -D @sveltejs/adapter-node
# Vercelnpm install -D @sveltejs/adapter-vercel
# Cloudflare Pagesnpm install -D @sveltejs/adapter-cloudflare
# Static sitenpm install -D @sveltejs/adapter-staticConfigure in svelte.config.js:
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter() }};Then build and deploy:
npm run buildnode build # for adapter-nodeSvelteKit’s adapter system means you write your app once and deploy anywhere. The adapter handles platform-specific optimizations like edge functions, serverless bundling, or static prerendering.
Key Takeaways
SvelteKit gives you a full-stack framework with minimal boilerplate. Load functions handle data fetching with automatic type safety. Form actions provide server-side mutation handling with progressive enhancement. API endpoints serve external consumers. Server-only modules prevent accidental secret exposure. And the adapter system makes deployment flexible.
For new full-stack projects, SvelteKit offers a cohesive experience that avoids the “choose your own adventure” problem common in React’s ecosystem. Start building, and you’ll find the conventions get out of your way quickly.