
sveltekit
SvelteKit - Full-stack Svelte framework with file-based routing, SSR/SSG, form actions, and adapters for deployment
SvelteKit - Full-stack Svelte framework with file-based routing, SSR/SSG, form actions, and adapters for deployment
SvelteKit - Full-Stack Svelte Framework
Overview
SvelteKit is the official full-stack framework for Svelte, providing file-based routing, server-side rendering (SSR), static site generation (SSG), form handling with progressive enhancement, and deployment adapters for any platform.
Key Features:
- File-based routing: Automatic routes from
src/routes/directory structure - Load functions: Type-safe data fetching (
+page.ts,+page.server.ts) - Form actions: Native form handling with progressive enhancement
- SSR/SSG/SPA: Flexible rendering modes with per-route control
- Adapters: Deploy to Vercel, Netlify, Node.js, Cloudflare, and more
- TypeScript-first: Generated types from
$typesfor type safety - Hooks: Middleware-like
handle,handleError,handleFetch - API routes:
+server.tsfiles for REST endpoints
Installation:
# Create new SvelteKit project
npm create svelte@latest my-app
cd my-app
npm install
npm run dev -- --open
# Templates: skeleton, demo app, library
# Choices: TypeScript, ESLint, Prettier, Playwright, Vitest
Project Structure
Standard SvelteKit Layout
my-sveltekit-app/
├── src/
│ ├── routes/ # File-based routing
│ │ ├── +page.svelte # / (home page)
│ │ ├── +page.ts # Universal load function
│ │ ├── +page.server.ts # Server-only load function
│ │ ├── +layout.svelte # Shared layout
│ │ ├── +layout.ts # Layout load function
│ │ ├── +error.svelte # Error page
│ │ ├── about/
│ │ │ └── +page.svelte # /about
│ │ ├── blog/
│ │ │ ├── +page.svelte # /blog (list)
│ │ │ ├── +page.server.ts # Load posts
│ │ │ └── [slug]/
│ │ │ ├── +page.svelte # /blog/my-post
│ │ │ └── +page.server.ts
│ │ └── api/
│ │ └── posts/
│ │ └── +server.ts # GET /api/posts
│ ├── lib/
│ │ ├── components/
│ │ ├── server/ # Server-only utilities
│ │ │ └── database.ts
│ │ ├── stores/
│ │ └── utils/
│ ├── hooks.server.ts # Server hooks
│ ├── hooks.client.ts # Client hooks
│ ├── app.html # HTML template
│ └── app.d.ts # TypeScript declarations
├── static/ # Static assets (robots.txt, favicon)
├── tests/ # Playwright tests
├── svelte.config.js # SvelteKit configuration
├── vite.config.ts # Vite configuration
└── package.json
File-Based Routing
Route Conventions
File naming determines routing:
| File | Route | Purpose |
|---|---|---|
+page.svelte |
/ |
Page component |
+page.ts |
- | Universal load (client + server) |
+page.server.ts |
- | Server-only load |
+layout.svelte |
- | Shared layout |
+layout.ts |
- | Layout load |
+layout.server.ts |
- | Server layout load |
+server.ts |
/api/... |
API endpoint (GET/POST/etc) |
+error.svelte |
- | Error boundary |
Basic Routes
src/routes/
├── +page.svelte # / (home)
├── about/
│ └── +page.svelte # /about
├── contact/
│ └── +page.svelte # /contact
└── pricing/
└── +page.svelte # /pricing
Dynamic Routes
src/routes/
└── blog/
├── +page.svelte # /blog (list)
├── [slug]/
│ └── +page.svelte # /blog/my-post
└── [category]/
└── [slug]/
└── +page.svelte # /blog/tech/my-post
Access route params:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<article>
<h1>{data.post.title}</h1>
<p>{data.post.content}</p>
</article>
Optional Parameters
src/routes/
└── archive/
└── [[year]]/
└── [[month]]/
└── +page.svelte # /archive, /archive/2024, /archive/2024/11
// src/routes/archive/[[year]]/[[month]]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const year = params.year || new Date().getFullYear();
const month = params.month || null;
return {
year,
month,
posts: await fetchPosts({ year, month })
};
};
Rest Parameters
src/routes/
└── docs/
└── [...path]/
└── +page.svelte # /docs/guide/intro, /docs/api/reference
// src/routes/docs/[...path]/+page.ts
export const load: PageLoad = async ({ params }) => {
const path = params.path; // "guide/intro"
const segments = path.split('/'); // ["guide", "intro"]
return {
doc: await fetchDoc(path)
};
};
Load Functions
Universal Load (+page.ts)
Runs on both server and client. Must use fetch for data fetching.
// src/routes/products/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, params, url }) => {
const response = await fetch('/api/products');
const products = await response.json();
return {
products,
searchQuery: url.searchParams.get('q') || ''
};
};
// Prerendering options
export const prerender = true; // Static generation
export const ssr = false; // Disable SSR (SPA mode)
export const csr = true; // Enable client-side rendering
Server-Only Load (+page.server.ts)
Runs only on server. Direct database access allowed.
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async ({ locals, cookies }) => {
// Check authentication
if (!locals.user) {
throw redirect(303, '/login');
}
// Direct database query (server-only)
const stats = await db.query.stats.findFirst({
where: eq(stats.userId, locals.user.id)
});
// Sensitive data stays on server
const apiKey = process.env.SECRET_API_KEY;
const data = await fetchPrivateData(apiKey);
return {
stats,
userData: data
};
};
Streaming with Promises
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
return {
// Immediate data
featured: await db.posts.findMany({ where: { featured: true } }),
// Streamed data (loads async)
recent: db.posts.findMany({ orderBy: { createdAt: 'desc' } }),
popular: db.posts.findMany({ orderBy: { views: 'desc' } })
};
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<h2>Featured</h2>
{#each data.featured as post}
<article>{post.title}</article>
{/each}
<h2>Recent</h2>
{#await data.recent}
<p>Loading recent posts...</p>
{:then posts}
{#each posts as post}
<article>{post.title}</article>
{/each}
{/await}
<h2>Popular</h2>
{#await data.popular}
<p>Loading popular posts...</p>
{:then posts}
{#each posts as post}
<article>{post.title}</article>
{/each}
{/await}
Layouts
Shared Layout
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import type { LayoutData } from './$types';
let { data, children } = $props<{ data: LayoutData, children: any }>();
</script>
<div class="app">
<Header user={data.user} />
<main>
{@render children()}
</main>
<Footer />
</div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
}
</style>
Layout Load
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user || null
};
};
Nested Layouts
src/routes/
├── +layout.svelte # Root layout (all pages)
├── (app)/
│ ├── +layout.svelte # App layout (dashboard, settings)
│ ├── dashboard/
│ │ └── +page.svelte # Uses: root + app layouts
│ └── settings/
│ └── +page.svelte
└── (marketing)/
├── +layout.svelte # Marketing layout (about, pricing)
├── about/
│ └── +page.svelte # Uses: root + marketing layouts
└── pricing/
└── +page.svelte
Layout groups with (name) don't affect URL structure:
/dashboardnot/(app)/dashboard
Breaking Out of Layouts
<!-- src/routes/admin/+layout.svelte -->
<script>
let { children } = $props();
</script>
<div class="admin">
{@render children()}
</div>
<!-- src/routes/admin/login/+page@.svelte -->
<!-- @ breaks out to root layout, skipping admin layout -->
<form method="POST">
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Login</button>
</form>
Form Actions
Basic Form Actions
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
// Default action (form without action attribute)
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) {
return fail(400, { email, missing: true });
}
const user = await authenticateUser(email, password);
if (!user) {
return fail(401, { email, incorrect: true });
}
cookies.set('session', user.sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7 // 1 week
});
throw redirect(303, '/dashboard');
}
} satisfies Actions;
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form } = $props<{ form?: ActionData }>();
</script>
<form method="POST">
<input
name="email"
type="email"
value={form?.email ?? ''}
required
/>
<input name="password" type="password" required />
{#if form?.missing}
<p class="error">Please fill in all fields</p>
{/if}
{#if form?.incorrect}
<p class="error">Invalid email or password</p>
{/if}
<button type="submit">Log in</button>
</form>
Named Actions
// src/routes/todos/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
return {
todos: await db.todos.findMany()
};
};
export const actions = {
create: async ({ request }) => {
const data = await request.formData();
const text = data.get('text');
if (!text) {
return fail(400, { text, missing: true });
}
await db.todos.create({ data: { text, done: false } });
return { success: true };
},
toggle: async ({ request }) => {
const data = await request.formData();
const id = data.get('id');
const todo = await db.todos.findUnique({ where: { id } });
await db.todos.update({
where: { id },
data: { done: !todo.done }
});
return { toggled: true };
},
delete: async ({ request }) => {
const data = await request.formData();
const id = data.get('id');
await db.todos.delete({ where: { id } });
return { deleted: true };
}
} satisfies Actions;
<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
import type { PageData, ActionData } from './$types';
let { data, form } = $props<{ data: PageData, form?: ActionData }>();
</script>
<h1>Todos</h1>
{#if form?.success}
<p class="success">Todo created!</p>
{/if}
<form method="POST" action="?/create">
<input name="text" placeholder="What needs to be done?" required />
<button type="submit">Add</button>
</form>
{#each data.todos as todo}
<div>
<form method="POST" action="?/toggle">
<input type="hidden" name="id" value={todo.id} />
<input
type="checkbox"
checked={todo.done}
onchange={(e) => e.currentTarget.form?.requestSubmit()}
/>
<span class:done={todo.done}>{todo.text}</span>
</form>
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={todo.id} />
<button type="submit">Delete</button>
</form>
</div>
{/each}
<style>
.done {
text-decoration: line-through;
opacity: 0.6;
}
</style>
Progressive Enhancement
<!-- src/routes/search/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
let { data, form } = $props<{ data: PageData, form?: ActionData }>();
let isLoading = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
isLoading = true;
return async ({ update }) => {
await update();
isLoading = false;
};
}}
>
<input name="query" placeholder="Search..." />
<button type="submit" disabled={isLoading}>
{isLoading ? 'Searching...' : 'Search'}
</button>
</form>
{#if form?.results}
<ul>
{#each form.results as result}
<li>{result.title}</li>
{/each}
</ul>
{/if}
API Routes (+server.ts)
REST Endpoints
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit')) || 10;
const offset = Number(url.searchParams.get('offset')) || 0;
const posts = await db.posts.findMany({
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' }
});
return json(posts);
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const data = await request.json();
const post = await db.posts.create({
data: {
title: data.title,
content: data.content,
authorId: locals.user.id
}
});
return json(post, { status: 201 });
};
Dynamic API Routes
// src/routes/api/posts/[id]/+server.ts
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
const post = await db.posts.findUnique({
where: { id: params.id }
});
if (!post) {
throw error(404, 'Post not found');
}
return json(post);
};
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
if (!locals.user) {
throw error(401, 'Unauthorized');
}
const post = await db.posts.findUnique({ where: { id: params.id } });
if (post.authorId !== locals.user.id) {
throw error(403, 'Forbidden');
}
const data = await request.json();
const updated = await db.posts.update({
where: { id: params.id },
data: { title: data.title, content: data.content }
});
return json(updated);
};
export const DELETE: RequestHandler = async ({ params, locals }) => {
if (!locals.user) {
throw error(401, 'Unauthorized');
}
const post = await db.posts.findUnique({ where: { id: params.id } });
if (post.authorId !== locals.user.id) {
throw error(403, 'Forbidden');
}
await db.posts.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
};
Hooks
Server Hooks (hooks.server.ts)
// src/hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
// Authentication middleware
const auth: Handle = async ({ event, resolve }) => {
const sessionToken = event.cookies.get('session');
if (sessionToken) {
event.locals.user = await getUserFromSession(sessionToken);
}
return resolve(event);
};
// Logging middleware
const logging: Handle = async ({ event, resolve }) => {
const start = Date.now();
const response = await resolve(event);
const duration = Date.now() - start;
console.log(`${event.request.method} ${event.url.pathname} ${response.status} ${duration}ms`);
return response;
};
// Protected routes middleware
const protect: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith('/admin')) {
if (!event.locals.user?.isAdmin) {
throw redirect(303, '/login');
}
}
if (event.url.pathname.startsWith('/dashboard')) {
if (!event.locals.user) {
throw redirect(303, '/login');
}
}
return resolve(event);
};
// Combine hooks in sequence
export const handle = sequence(auth, logging, protect);
// Error handling
export const handleError = async ({ error, event }) => {
console.error('Error:', error);
return {
message: 'An unexpected error occurred',
code: error?.code ?? 'UNKNOWN'
};
};
// Fetch handling (modify requests)
export const handleFetch = async ({ request, fetch }) => {
// Add auth headers to internal API calls
if (request.url.startsWith('https://api.example.com')) {
request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
}
return fetch(request);
};
Client Hooks (hooks.client.ts)
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({ error, event }) => {
console.error('Client error:', error);
// Send to error tracking service
if (typeof window !== 'undefined') {
// Sentry, LogRocket, etc.
}
return {
message: 'Something went wrong',
};
};
Environment Variables
Static Environment Variables
// src/lib/config.ts
import { env } from '$env/static/public';
import { env as privateEnv } from '$env/static/private';
// Public variables (available in browser)
export const PUBLIC_API_URL = env.PUBLIC_API_URL;
export const PUBLIC_SITE_NAME = env.PUBLIC_SITE_NAME;
// Private variables (server-only)
export const DATABASE_URL = privateEnv.DATABASE_URL;
export const SECRET_KEY = privateEnv.SECRET_KEY;
Dynamic Environment Variables
// src/routes/+page.server.ts
import { env } from '$env/dynamic/private';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
// Can change at runtime
const apiUrl = env.API_URL;
return {
data: await fetch(apiUrl).then(r => r.json())
};
};
Environment file (.env):
# Public (exposed to browser)
PUBLIC_API_URL=https://api.example.com
PUBLIC_ANALYTICS_ID=UA-123456789
# Private (server-only)
DATABASE_URL=postgres://localhost:5432/mydb
SECRET_KEY=super-secret-key
STRIPE_SECRET_KEY=sk_live_abc123
Prerendering and SSR
Prerendering Options
// src/routes/blog/+page.ts
export const prerender = true; // Prerender at build time
export const ssr = true; // Server-side render (default)
export const csr = true; // Client-side render (default)
Prerender entire site:
// svelte.config.js
export default {
kit: {
prerender: {
entries: ['*'],
crawl: true
}
}
};
Dynamic Prerendering
// src/routes/blog/[slug]/+page.server.ts
import type { EntryGenerator, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await db.posts.findUnique({
where: { slug: params.slug }
});
return { post };
};
// Generate static pages for all posts at build time
export const entries: EntryGenerator = async () => {
const posts = await db.posts.findMany();
return posts.map(post => ({
slug: post.slug
}));
};
export const prerender = true;
Adapters
Vercel Adapter
npm install -D @sveltejs/adapter-vercel
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
export default {
kit: {
adapter: adapter({
runtime: 'edge', // or 'nodejs'
regions: ['iad1', 'sfo1'],
split: false
})
}
};
Netlify Adapter
npm install -D @sveltejs/adapter-netlify
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
export default {
kit: {
adapter: adapter({
edge: false, // true for edge functions
split: false
})
}
};
Node Adapter
npm install -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({
out: 'build',
precompress: true,
envPrefix: 'MY_'
})
}
};
Run production server:
npm run build
node build
Static Adapter (SSG)
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: '200.html', // SPA fallback
precompress: false
})
}
};
Cloudflare Pages
npm install -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
export default {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<build>']
}
})
}
};
Testing
Unit Tests with Vitest
// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './utils';
describe('formatDate', () => {
it('formats date correctly', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('January 15, 2024');
});
});
Component Tests
// src/lib/components/Button.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';
describe('Button', () => {
it('renders with text', () => {
const { getByText } = render(Button, {
props: { text: 'Click me' }
});
expect(getByText('Click me')).toBeInTheDocument();
});
it('calls onclick handler', async () => {
const handleClick = vi.fn();
const { getByText } = render(Button, {
props: { text: 'Click me', onclick: handleClick }
});
const button = getByText('Click me');
await fireEvent.click(button);
expect(handleClick).toHaveBeenCalledOnce();
});
});
E2E Tests with Playwright
// tests/login.test.ts
import { expect, test } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('login validation works', async ({ page }) => {
await page.goto('/login');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Please fill in all fields');
});
Load Function Tests
// src/routes/blog/+page.server.test.ts
import { describe, it, expect, vi } from 'vitest';
import { load } from './+page.server';
vi.mock('$lib/server/database', () => ({
db: {
posts: {
findMany: vi.fn(() => Promise.resolve([
{ id: '1', title: 'Post 1' },
{ id: '2', title: 'Post 2' }
]))
}
}
}));
describe('blog page load', () => {
it('loads posts', async () => {
const result = await load({ params: {}, url: new URL('http://localhost') } as any);
expect(result.posts).toHaveLength(2);
expect(result.posts[0].title).toBe('Post 1');
});
});
Advanced Patterns
Parallel Loading
// src/routes/dashboard/+layout.server.ts
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
// Load data in parallel
const [user, notifications, settings] = await Promise.all([
db.users.findUnique({ where: { id: locals.user.id } }),
db.notifications.findMany({ where: { userId: locals.user.id } }),
db.settings.findUnique({ where: { userId: locals.user.id } })
]);
return {
user,
notifications,
settings
};
};
Dependent Loading
// src/routes/profile/[username]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, parent }) => {
// Wait for parent layout data
const { user } = await parent();
const profile = await db.profiles.findUnique({
where: { username: params.username }
});
if (!profile) {
throw error(404, 'Profile not found');
}
// Load posts only if profile exists
const posts = await db.posts.findMany({
where: { authorId: profile.id },
orderBy: { createdAt: 'desc' }
});
return {
profile,
posts,
isOwnProfile: user?.id === profile.id
};
};
Invalidation and Reloading
<script lang="ts">
import { invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
async function refresh() {
// Reload current page data
await invalidateAll();
}
async function refreshPosts() {
// Reload specific data
await invalidate('/api/posts');
}
async function refreshUser() {
// Reload data depending on specific URL
await invalidate(url => url.pathname.startsWith('/api/user'));
}
</script>
<button onclick={refresh}>Refresh All</button>
<button onclick={refreshPosts}>Refresh Posts</button>
Page Options
// src/routes/admin/+page.ts
export const ssr = false; // Disable server-side rendering
export const csr = true; // Enable client-side rendering
export const prerender = false; // Disable prerendering
export const trailingSlash = 'always'; // /page/ instead of /page
Deployment Examples
Vercel Deployment
# Install Vercel CLI
npm install -g vercel
# Login
vercel login
# Deploy
vercel
# Production deploy
vercel --prod
vercel.json:
{
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"framework": "sveltekit"
}
Docker Deployment (Node Adapter)
Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]
docker build -t my-sveltekit-app .
docker run -p 3000:3000 my-sveltekit-app
Static Hosting (Netlify, GitHub Pages)
# Build static site
npm run build
# Output in build/ directory
# Deploy build/ to static host
netlify.toml:
[build]
command = "npm run build"
publish = "build"
[[redirects]]
from = "/*"
to = "/200.html"
status = 200
Best Practices
- Use
+page.server.tsfor sensitive operations - Keep secrets server-side - Leverage progressive enhancement - Forms work without JavaScript
- Use
$typesfor type safety - Auto-generated types from SvelteKit - Implement error boundaries - Use
+error.sveltefor graceful errors - Optimize images - Use
@sveltejs/enhanced-imgfor automatic optimization - Enable prerendering - Static pages are faster and cheaper
- Use parallel loading -
Promise.all()for concurrent data fetching - Validate form data - Use Zod or similar for schema validation
- Set security headers - Use hooks for CSP, CORS, etc.
- Test with Playwright - E2E tests prevent regressions
Common Patterns
Authentication Flow
// src/routes/login/+page.server.ts
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const user = await authenticate(data.get('email'), data.get('password'));
cookies.set('session', user.sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: true,
maxAge: 60 * 60 * 24 * 7
});
throw redirect(303, '/dashboard');
}
};
// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
const sessionToken = event.cookies.get('session');
if (sessionToken) {
event.locals.user = await getUserFromSession(sessionToken);
}
return resolve(event);
};
Protected Routes
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
return {
user: locals.user
};
};
Form Validation
// src/routes/register/+page.server.ts
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
import type { Actions } from './$types';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword']
});
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const formData = Object.fromEntries(data);
const result = schema.safeParse(formData);
if (!result.success) {
return fail(400, {
errors: result.error.flatten().fieldErrors,
data: formData
});
}
await createUser(result.data);
throw redirect(303, '/login');
}
} satisfies Actions;
Resources
- SvelteKit Docs: https://kit.svelte.dev/docs
- Svelte Tutorial: https://learn.svelte.dev
- Adapters: https://kit.svelte.dev/docs/adapters
- Deployment: https://kit.svelte.dev/docs/adapter-auto
- Discord: https://svelte.dev/chat
Summary
- SvelteKit is the official full-stack framework for Svelte
- File-based routing with
+page.svelte,+layout.svelte,+server.ts - Load functions provide type-safe data fetching (universal and server-only)
- Form actions enable progressive enhancement with native HTML forms
- SSR/SSG/SPA modes with per-route control via
prerender,ssr,csr - Adapters deploy to any platform (Vercel, Netlify, Node, Cloudflare, static)
- Hooks provide middleware-like functionality for auth, logging, error handling
- TypeScript-first with auto-generated
$typesfor complete type safety - Environment variables with
$env/staticand$env/dynamicmodules - Testing with Vitest (unit) and Playwright (E2E)
You Might Also Like
Related Skills

verify
Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
facebook
test
Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
facebook
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
facebook
extract-errors
Use when adding new error messages to React, or seeing "unknown error code" warnings.
facebook