17.6 SvelteKit Fundamentals
SvelteKit is Svelte's official full-stack framework. It supports file-based routing, server-side rendering (SSR), static site generation (SSG), and API endpoints.
SvelteKit Architecture
Browser request
↓
SvelteKit router (file-based)
↓
load function executes (server or client)
↓
Page component renders
↓
Deliver HTML + JS to client
SvelteKit can decide the rendering mode per request:
- SSR: Generate HTML on server and deliver (default)
- SSG: Pre-generate HTML at build time
- CSR: Render entirely on the client
File-Based Routing
SvelteKit generates routes from the src/routes/ directory structure.
src/routes/
├── +layout.svelte # Root layout (applies to all pages)
├── +layout.js # Root layout data loading
├── +page.svelte # / (home)
├── +error.svelte # Error page
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ ├── +page.js # /blog data loading
│ └── [slug]/
│ ├── +page.svelte # /blog/:slug
│ └── +page.server.js # /blog/:slug server data
├── api/
│ └── users/
│ └── +server.js # /api/users API endpoint
└── (auth)/ # Group (no effect on URL)
├── login/
│ └── +page.svelte # /login
└── register/
└── +page.svelte # /register
Core File Types
| File | Role | Execution Environment |
|---|---|---|
+page.svelte | Page component | Client |
+page.js | Page data loading | Server + Client |
+page.server.js | Server-only data/actions | Server only |
+layout.svelte | Layout component | Client |
+layout.js | Layout data loading | Server + Client |
+layout.server.js | Server-only layout data | Server only |
+server.js | API endpoint | Server only |
+error.svelte | Error page | Client |
+page.svelte Basics
<!-- src/routes/+page.svelte -->
<script>
// Receive load function's return value as data prop
let { data } = $props();
</script>
<svelte:head>
<title>Home Page</title>
<meta name="description" content="SvelteKit example home page" />
</svelte:head>
<main>
<h1>Welcome!</h1>
<p>Message from server: {data.message}</p>
<p>User count: {data.userCount}</p>
</main>
+layout.svelte — Layout
<!-- src/routes/+layout.svelte -->
<script>
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
let { data, children } = $props();
</script>
<Header user={data.user} />
<main>
{@render children()}
</main>
<Footer />
<style>
main {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
min-height: calc(100vh - 120px);
}
</style>
Data Loading: load Functions
+page.js — Universal load Function
Runs on both server and client.
// src/routes/blog/+page.js
export async function load({ fetch, url }) {
const page = Number(url.searchParams.get('page') ?? 1);
const res = await fetch(`/api/posts?page=${page}&limit=10`);
if (!res.ok) throw new Error('Could not load posts.');
const { posts, total } = await res.json();
return { posts, total, page };
}
<!-- src/routes/blog/+page.svelte -->
<script>
let { data } = $props();
</script>
<h1>Blog ({data.total} posts)</h1>
{#each data.posts as post}
<article>
<h2><a href="/blog/{post.slug}">{post.title}</a></h2>
<p>{post.excerpt}</p>
<time>{post.publishedAt}</time>
</article>
{/each}
<div class="pagination">
{#if data.page > 1}
<a href="?page={data.page - 1}">← Previous</a>
{/if}
<span>Page {data.page}</span>
{#if data.posts.length === 10}
<a href="?page={data.page + 1}">Next →</a>
{/if}
</div>
+page.server.js — Server-Only load Function
Used for direct DB access, private API keys, or anything that must run only on the server:
// src/routes/blog/[slug]/+page.server.js
import { db } from '$lib/server/db.js';
import { error } from '@sveltejs/kit';
export async function load({ params }) {
const post = await db.post.findUnique({
where: { slug: params.slug },
include: { author: true, tags: true },
});
if (!post) {
throw error(404, `Post '${params.slug}' not found.`);
}
// Exclude sensitive fields before returning
return {
post: {
title: post.title,
content: post.content,
publishedAt: post.publishedAt.toISOString(),
author: { name: post.author.name },
tags: post.tags.map(t => t.name),
},
};
}
Form Actions
Handles HTML forms on the server. Works without JavaScript; provides enhanced experience when JS is available.
// src/routes/contact/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import { sendEmail } from '$lib/server/email.js';
export const actions = {
// Default action (when form has no action attribute)
default: async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name')?.toString().trim();
const email = formData.get('email')?.toString().trim();
const message = formData.get('message')?.toString().trim();
const errors = {};
if (!name) errors.name = 'Name is required.';
if (!email || !email.includes('@')) errors.email = 'Please enter a valid email.';
if (!message) errors.message = 'Message is required.';
if (Object.keys(errors).length > 0) {
return fail(400, { errors, values: { name, email, message } });
}
try {
await sendEmail({ name, email, message });
throw redirect(303, '/contact/success');
} catch (err) {
if (err instanceof Response) throw err;
return fail(500, { errors: { general: 'Failed to send email.' } });
}
},
};
<!-- src/routes/contact/+page.svelte -->
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<h1>Contact Us</h1>
<form method="POST" use:enhance>
{#if form?.errors?.general}
<p class="error">{form.errors.general}</p>
{/if}
<div>
<label for="name">Name</label>
<input id="name" name="name" value={form?.values?.name ?? ''} required />
{#if form?.errors?.name}
<p class="field-error">{form.errors.name}</p>
{/if}
</div>
<div>
<label for="email">Email</label>
<input id="email" name="email" type="email" value={form?.values?.email ?? ''} required />
{#if form?.errors?.email}
<p class="field-error">{form.errors.email}</p>
{/if}
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required>{form?.values?.message ?? ''}</textarea>
{#if form?.errors?.message}
<p class="field-error">{form.errors.message}</p>
{/if}
</div>
<button type="submit">Send</button>
</form>
API Endpoints (+server.js)
// src/routes/api/posts/+server.js
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';
// GET /api/posts
export async function GET({ url }) {
const page = Number(url.searchParams.get('page') ?? 1);
const limit = Number(url.searchParams.get('limit') ?? 10);
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
db.post.findMany({ skip, take: limit, orderBy: { publishedAt: 'desc' } }),
db.post.count(),
]);
return json({ posts, total, page, limit });
}
// POST /api/posts
export async function POST({ request, locals }) {
if (!locals.user) throw error(401, 'Login required.');
const body = await request.json();
const { title, content, tags } = body;
if (!title || !content) throw error(400, 'Title and content are required.');
const post = await db.post.create({
data: {
title,
content,
slug: title.toLowerCase().replace(/\s+/g, '-'),
authorId: locals.user.id,
tags: { connect: tags?.map(name => ({ name })) ?? [] },
},
});
return json(post, { status: 201 });
}
// src/routes/api/posts/[id]/+server.js
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';
export async function GET({ params }) {
const post = await db.post.findUnique({ where: { id: Number(params.id) } });
if (!post) throw error(404, 'Post not found.');
return json(post);
}
export async function PUT({ params, request, locals }) {
if (!locals.user) throw error(401, 'Login required.');
const body = await request.json();
const post = await db.post.update({ where: { id: Number(params.id) }, data: body });
return json(post);
}
export async function DELETE({ params, locals }) {
if (!locals.user) throw error(401, 'Login required.');
await db.post.delete({ where: { id: Number(params.id) } });
return new Response(null, { status: 204 });
}
Dynamic Routes
src/routes/
├── blog/[slug]/ # /blog/hello-world
├── shop/[category]/[id]/ # /shop/electronics/123
└── docs/[...path]/ # /docs/a/b/c (rest parameter)
// src/routes/blog/[slug]/+page.server.js
export async function load({ params }) {
// params.slug = 'hello-world'
const post = await getPostBySlug(params.slug);
return { post };
}
Optional Parameters
src/routes/[[lang]]/about/ # Matches both /about and /en/about
export async function load({ params }) {
const lang = params.lang ?? 'en'; // Default value
return { lang };
}
Nested Layouts
src/routes/
├── +layout.svelte # Root layout (Header, Footer)
├── +page.svelte # Home
└── admin/
├── +layout.svelte # Admin layout (Sidebar)
├── +layout.server.js # Admin auth check
├── +page.svelte # /admin
└── users/
└── +page.svelte # /admin/users
<!-- src/routes/admin/+layout.svelte -->
<script>
let { data, children } = $props();
</script>
<div class="admin-layout">
<aside class="sidebar">
<nav>
<a href="/admin">Dashboard</a>
<a href="/admin/users">Users</a>
<a href="/admin/posts">Posts</a>
<a href="/admin/settings">Settings</a>
</nav>
</aside>
<div class="content">
{@render children()}
</div>
</div>
// src/routes/admin/+layout.server.js
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) {
if (!locals.user || locals.user.role !== 'admin') {
throw redirect(303, '/login?redirect=/admin');
}
return { user: locals.user };
}
Adapter Configuration
Cloudflare Pages (Recommended)
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
export default {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>'],
},
}),
},
};
Static Site Generation
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
}),
prerender: {
entries: ['*'],
},
},
};
Per-page SSG configuration:
// src/routes/blog/+page.js
export const prerender = true; // Pre-render this page
export const ssr = true; // Enable SSR (default)
export const csr = true; // Enable CSR (default)
Practical Example: Blog Site
// src/routes/blog/+page.js
export async function load({ fetch }) {
const res = await fetch('/api/posts?limit=10');
return await res.json();
}
<!-- src/routes/blog/+page.svelte -->
<script>
import { formatDate } from '$lib/utils/date.js';
let { data } = $props();
</script>
<svelte:head>
<title>Blog</title>
</svelte:head>
<h1>Blog</h1>
<div class="posts-grid">
{#each data.posts as post (post.id)}
<article class="post-card">
{#if post.coverImage}
<img src={post.coverImage} alt={post.title} />
{/if}
<div class="content">
<div class="tags">
{#each post.tags as tag}
<span class="tag">{tag}</span>
{/each}
</div>
<h2><a href="/blog/{post.slug}">{post.title}</a></h2>
<p>{post.excerpt}</p>
<footer>
<span>{post.author.name}</span>
<time>{formatDate(post.publishedAt)}</time>
</footer>
</div>
</article>
{/each}
</div>
<style>
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.post-card { border: 1px solid #eee; border-radius: 12px; overflow: hidden; transition: box-shadow 0.2s; }
.post-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
.post-card img { width: 100%; height: 200px; object-fit: cover; }
.content { padding: 1.25rem; }
.tags { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.tag { background: #f0f0f0; padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; }
h2 a { text-decoration: none; color: inherit; }
h2 a:hover { color: #ff3e00; }
footer { display: flex; justify-content: space-between; font-size: 0.85rem; color: #666; margin-top: 1rem; }
</style>
Pro Tips
Tip 1: hooks.server.js — Server Middleware
// src/hooks.server.js
import { db } from '$lib/server/db.js';
export async function handle({ event, resolve }) {
// Check session on every request
const sessionId = event.cookies.get('session');
if (sessionId) {
event.locals.user = await db.session.findUnique({
where: { id: sessionId },
include: { user: true },
}).then(s => s?.user ?? null);
}
const response = await resolve(event);
return response;
}
Tip 2: Custom Error Pages
<!-- src/routes/+error.svelte -->
<script>
import { page } from '$app/stores';
</script>
<svelte:head>
<title>Error {$page.status}</title>
</svelte:head>
<div class="error-page">
<h1>{$page.status}</h1>
<p>{$page.error?.message ?? 'An unknown error occurred.'}</p>
<a href="/">Go Home</a>
</div>
Tip 3: Navigation State
<script>
import { navigating, page } from '$app/stores';
</script>
<!-- Show loading indicator during navigation -->
{#if $navigating}
<div class="loading-bar"></div>
{/if}
<!-- Active links based on current path -->
<nav>
{#each [['/', 'Home'], ['/blog', 'Blog'], ['/about', 'About']] as [href, label]}
<a {href} class:active={$page.url.pathname === href}>{label}</a>
{/each}
</nav>
Summary
| Concept | Description |
|---|---|
| File-based routing | src/routes/ structure determines URL structure |
+page.svelte | Page UI component |
+layout.svelte | Shared layout |
load function | Prepare data before page renders |
| Form Actions | Handle forms on the server |
+server.js | REST API endpoint |
[slug] | Dynamic URL parameter |
| Adapter | Optimization for deployment environment |
The next chapter covers practical pro tips for Svelte and SvelteKit.