The fastest way to build web apps. File-based routing, server-side rendering, and progressive enhancement by default.
๐ Project Structure
my-app/
โโโ src/
โ โโโ lib/
โ โ โโโ components/ # Reusable components
โ โ โโโ server/ # Server-only modules
โ โ โโโ stores/ # State management
โ โ โโโ utils/ # Helper functions
โ โโโ routes/
โ โ โโโ +page.svelte # Home page
โ โ โโโ +page.ts # Page load function
โ โ โโโ +layout.svelte # Root layout
โ โ โโโ +layout.ts # Layout load
โ โ โโโ +error.svelte # Error boundary
โ โ โโโ api/
โ โ โ โโโ users/
โ โ โ โโโ +server.ts # API endpoint
โ โ โโโ (group)/
โ โ โโโ +layout.svelte # Route group
โ โโโ app.html # HTML template
โ โโโ app.d.ts # Type declarations
โโโ static/ # Static assets
โโโ tests/
โโโ svelte.config.js
โโโ vite.config.ts
โโโ tsconfig.json
๐ File Naming Convention
| File | Purpose |
|---|---|
+page.svelte | Page component |
+page.ts | Universal load function |
+page.server.ts | Server-only load |
+layout.svelte | Layout wrapper |
+layout.ts | Layout load data |
+layout.server.ts | Server layout load |
+error.svelte | Error UI |
+server.ts | API endpoint |
+hooks.ts | Client hooks |
+hooks.server.ts | Server hooks |
๐ Data Loading
Universal Load (+page.ts)
Runs on server first, then client for SPA navigation.
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from "./$types"
import { error } from "@sveltejs/kit"
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`)
if (!response.ok) {
throw error(response.status, "Post not found")
}
return {
post: await response.json(),
streamed: {
comments: fetch(`/api/comments/${params.slug}`).then((r) => r.json()),
},
}
}<!-- +page.svelte -->
<script>
let { data } = $props()
</script>
<h1>{data.post.title}</h1>
{#await data.streamed.comments}
<p>Loading comments...</p>
{:then comments}
<CommentList {comments} />
{/await}Server Load (+page.server.ts)
Server-only, can use private env vars, database directly.
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types"
import { db } from "$lib/server/db"
import { redirect } from "@sveltejs/kit"
export const load: PageServerLoad = async ({ locals, url }) => {
// Check auth
if (!locals.user) {
throw redirect(302, `/login?redirectTo=${url.pathname}`)
}
// Direct DB access (no API call needed)
const projects = await db.query("SELECT * FROM projects WHERE user_id = $1", [locals.user.id])
return {
projects,
user: locals.user,
}
}Layout Data
// src/routes/+layout.ts
import type { LayoutLoad } from "./$types"
export const load: LayoutLoad = async ({ fetch }) => {
const settings = await fetch("/api/settings").then((r) => r.json())
return {
settings,
// Available to all child routes
}
}<!-- +layout.svelte -->
<script>
let { data, children } = $props()
</script>
<Nav theme={data.settings.theme} />
{@render children()}
<Footer />๐ API Endpoints
REST API (+server.ts)
// src/routes/api/users/+server.ts
import type { RequestHandler } from "./$types"
import { json, error } from "@sveltejs/kit"
import { db } from "$lib/server/db"
// GET /api/users
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get("page")) || 1
const limit = Math.min(Number(url.searchParams.get("limit")) || 10, 100)
const users = await db.users.findMany({
take: limit,
skip: (page - 1) * limit,
})
return json({ users, page, limit })
}
// POST /api/users
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json()
// Validation
if (!body.email?.includes("@")) {
throw error(400, "Valid email required")
}
const user = await db.users.create({ data: body })
return json(user, { status: 201 })
}
// PATCH /api/users/:id
// DELETE /api/users/:idForm Actions (+page.server.ts)
// src/routes/login/+page.server.ts
import type { Actions } from "./$types"
import { fail, redirect } from "@sveltejs/kit"
import { auth } from "$lib/server/auth"
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData()
const email = data.get("email")
const password = data.get("password")
// Validation
if (!email || !password) {
return fail(400, {
email: email?.toString() || "",
error: "Email and password required",
})
}
// Auth
const user = await auth.verify(email.toString(), password.toString())
if (!user) {
return fail(400, {
email: email.toString(),
error: "Invalid credentials",
})
}
// Set session
const session = await auth.createSession(user.id)
cookies.set("sessionid", session.id, {
path: "/",
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7, // 1 week
})
throw redirect(302, "/dashboard")
},
}<!-- +page.svelte -->
<script>
let { form } = $props()
</script>
<form method="POST">
<input name="email" type="email" value={form?.email ?? ''} />
<input name="password" type="password" />
<button type="submit">Login</button>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
</form>Named Actions
// Multiple actions on same page
export const actions: Actions = {
create: async ({ request }) => {
/* ... */
},
update: async ({ request }) => {
/* ... */
},
delete: async ({ request }) => {
/* ... */
},
}<!-- Use ?/actionName -->
<form method="POST" action="?/create">
<button>Create</button>
</form>
<form method="POST" action="?/delete">
<button>Delete</button>
</form>๐ Authentication & Hooks
Server Hooks (hooks.server.ts)
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit"
import { auth } from "$lib/server/auth"
export const handle: Handle = async ({ event, resolve }) => {
// Get session from cookie
const sessionId = event.cookies.get("sessionid")
if (sessionId) {
const session = await auth.getSession(sessionId)
if (session) {
event.locals.user = session.user
event.locals.session = session
}
}
// Protected routes
if (event.url.pathname.startsWith("/admin")) {
if (!event.locals.user?.isAdmin) {
return new Response("Unauthorized", { status: 403 })
}
}
const response = await resolve(event)
return response
}Type-Safe Locals
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user?: {
id: string
email: string
isAdmin: boolean
}
session?: {
id: string
expiresAt: Date
}
}
interface PageData {
// Automatic type from load functions
}
interface Error {
message: string
code?: string
}
}
}
export {}๐ Advanced Patterns
Streaming
// +page.server.ts
export const load: PageServerLoad = async () => {
return {
// Stream slow data
slowData: new Promise((resolve) => {
setTimeout(() => resolve("loaded!"), 2000)
}),
}
}<!-- Automatic streaming -->
{#await data.slowData}
<Skeleton />
{:then value}
<Content {value} />
{/await}Parallel Data Loading
// Fetch in parallel
export const load: PageLoad = async ({ fetch }) => {
const [users, posts, settings] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
fetch("/api/settings").then((r) => r.json()),
])
return { users, posts, settings }
}Route Groups
src/routes/
โโโ (marketing)/ # No URL segment
โ โโโ +layout.svelte # Marketing layout
โ โโโ +page.svelte # /
โ โโโ about/
โ โ โโโ +page.svelte # /about
โ โโโ pricing/
โ โโโ +page.svelte # /pricing
โโโ (app)/
โ โโโ +layout.svelte # App layout (with sidebar)
โ โโโ dashboard/
โ โ โโโ +page.svelte # /dashboard
โ โโโ settings/
โ โโโ +page.svelte # /settings
โโโ +layout.svelte # Root layout
Dynamic Routes
// src/routes/blog/[slug]/+page.ts
export const load: PageLoad = async ({ params }) => {
// params.slug available
console.log(params.slug) // 'my-post'
}// src/routes/blog/[...path]/+page.ts
// Matches: /blog/a, /blog/a/b, /blog/a/b/c
export const load: PageLoad = async ({ params }) => {
console.log(params.path) // 'a/b/c'
}Optional Parameters
src/routes/
โโโ lang/
โโโ [[lang]]/
โโโ +page.svelte # Matches: /lang, /lang/en, /lang/de
โก Configuration
Svelte Config
// svelte.config.js
import adapter from "@sveltejs/adapter-auto"
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$lib: "./src/lib",
"$lib/*": "./src/lib/*",
$components: "./src/lib/components",
$stores: "./src/lib/stores",
},
files: {
assets: "static",
lib: "src/lib",
routes: "src/routes",
appTemplate: "src/app.html",
},
},
}
export default configAdapters
// Vercel
import adapter from "@sveltejs/adapter-vercel"
// Netlify
import adapter from "@sveltejs/adapter-netlify"
// Static (SPA mode)
import adapter from "@sveltejs/adapter-static"
// Node server
import adapter from "@sveltejs/adapter-node"๐งช Testing
// src/routes/api/users/+server.test.ts
import { describe, it, expect } from "vitest"
import { GET } from "./+server"
describe("/api/users", () => {
it("returns users list", async () => {
const response = await GET({ url: new URL("http://localhost/api/users") })
const data = await response.json()
expect(data.users).toBeDefined()
expect(Array.isArray(data.users)).toBe(true)
})
})๐ฏ Best Practices
- Use server loads for private data โ no API calls needed
- Stream slow data โ better perceived performance
- Validate forms server-side โ always, then enhance client-side
- Type your locals โ extends App.Locals in app.d.ts
- Use route groups โ share layouts without URL segments
- Handle errors gracefully โ +error.svelte boundaries
- Progressive enhancement โ forms work without JS