Skip to content
Vladimir Chavkov
Go back

Building Full-Stack Apps with SvelteKit: A Complete Guide

Edit page

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:

Terminal window
npx sv create my-app
cd my-app
npm install
npm run dev

The 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 shell
static/ # Static assets
svelte.config.js # SvelteKit config
vite.config.ts # Vite config

File-Based Routing

Every directory under src/routes/ becomes a URL path. Each route can contain:

FilePurpose
+page.sveltePage component (renders in browser)
+page.tsUniversal load function (runs on server and client)
+page.server.tsServer-only load function and form actions
+layout.svelteLayout wrapping child routes
+server.tsAPI endpoint (GET, POST, PUT, DELETE)
+error.svelteError 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 endpoint

Load 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:

src/lib/server/database.ts
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:

Terminal window
# Node.js server
npm install -D @sveltejs/adapter-node
# Vercel
npm install -D @sveltejs/adapter-vercel
# Cloudflare Pages
npm install -D @sveltejs/adapter-cloudflare
# Static site
npm install -D @sveltejs/adapter-static

Configure in svelte.config.js:

import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter()
}
};

Then build and deploy:

Terminal window
npm run build
node build # for adapter-node

SvelteKit’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.


Edit page
Share this post on:

Previous Post
Understanding Next.js App Router and React Server Components
Next Post
Svelte vs React: A Practical Comparison for 2025