SvelteKit - Kompletny Przewodnik po Fullstack Framework Svelte
Czym jest SvelteKit?
SvelteKit to oficjalny fullstack framework dla Svelte, zaprojektowany do tworzenia nowoczesnych aplikacji webowych. Łączy elegancję i prostotę Svelte z potężnymi możliwościami serwerowymi - SSR, file-based routing, form actions i API endpoints. To wszystko przy minimalnym boilerplate i niesamowitej wydajności dzięki unikalnemu podejściu kompilacyjnemu.
W przeciwieństwie do innych frameworków, Svelte kompiluje komponenty do natywnego JavaScript podczas budowania aplikacji, eliminując potrzebę dołączania runtime'u do paczki. Oznacza to mniejsze pliki, szybsze ładowanie i lepszą wydajność. SvelteKit wykorzystuje tę architekturę i dodaje do niej routing, data loading, form handling i deployment adaptery.
Dlaczego SvelteKit?
Kluczowe zalety SvelteKit
- Brak runtime - Kompilator generuje natywny JavaScript
- Najszybszy framework - Mniejsze paczki, szybsze hydration
- Prosty mental model - Intuicyjna składnia bez boilerplate
- File-based routing - Struktura folderów = struktura URL
- Form actions - Natywne formularze z progressive enhancement
- Uniwersalny - SSR, SSG, SPA, wszystko w jednym
- Svelte 5 Runes - Nowoczesny system reaktywności
- Vite pod spodem - Błyskawiczny dev server
SvelteKit vs Inne Frameworki
| Cecha | SvelteKit | Next.js | Nuxt | Remix |
|---|---|---|---|---|
| Runtime | Brak | React | Vue | React |
| Rozmiar bundle | Najmniejszy | Średni | Średni | Mały |
| Learning curve | Łatwa | Średnia | Średnia | Średnia |
| Form handling | Natywne | Client-side | Client-side | Natywne |
| Kompilator | Tak | Nie | Nie | Nie |
| Dev experience | Świetny | Dobry | Dobry | Dobry |
| TypeScript | Pełne | Pełne | Pełne | Pełne |
| Adaptery deploy | Wiele | Vercel-first | Wiele | Wiele |
Kiedy wybrać SvelteKit?
- Mniejsze zespoły - prostsza składnia, mniej kodu
- Wydajność krytyczna - najmniejsze bundle
- Progressive enhancement - natywne formularze
- Szybki development - HMR przez Vite
- Nowe projekty - nowoczesna architektura
Instalacja i Konfiguracja
Tworzenie nowego projektu
# Nowy projekt z oficjalnym CLI
npx sv create my-app
# Interaktywny wizard zapyta o:
# - Template (skeleton, demo app, library)
# - TypeScript (tak/nie)
# - Add-ons (Tailwind, ESLint, Prettier, Playwright, Vitest)
cd my-app
npm install
npm run devAlternatywne metody
# Z pnpm
pnpm create svelte@latest my-app
# Z yarn
yarn create svelte my-app
# Z bunx
bunx sv create my-appStruktura projektu
my-app/
├── src/
│ ├── app.html # HTML template
│ ├── app.css # Global styles
│ ├── app.d.ts # TypeScript declarations
│ ├── lib/ # Shared code ($lib alias)
│ │ ├── components/ # Komponenty
│ │ ├── server/ # Server-only code
│ │ └── utils/ # Helpers
│ └── routes/ # File-based routing
│ ├── +page.svelte # Strona główna
│ ├── +page.server.ts # Server load
│ ├── +layout.svelte # Layout
│ ├── +error.svelte # Error page
│ └── api/ # API endpoints
│ └── +server.ts
├── static/ # Static assets
├── svelte.config.js # Svelte config
├── vite.config.ts # Vite config
├── tsconfig.json
└── package.jsonKonfiguracja svelte.config.js
// svelte.config.js
import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Preprocessors (TypeScript, SCSS, etc.)
preprocess: vitePreprocess(),
kit: {
// Adapter do deploymentu
adapter: adapter(),
// Aliasy
alias: {
$components: 'src/lib/components',
$utils: 'src/lib/utils'
},
// Content Security Policy
csp: {
mode: 'auto',
directives: {
'script-src': ['self']
}
},
// Prerendering
prerender: {
handleHttpError: 'warn'
}
}
}
export default configFile-Based Routing
Podstawowa struktura
SvelteKit używa struktury folderów w src/routes/ do definiowania routingu:
src/routes/
├── +page.svelte # /
├── +layout.svelte # Layout dla wszystkich stron
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ ├── +page.server.ts # Data loading
│ └── [slug]/ # Dynamic route
│ ├── +page.svelte # /blog/:slug
│ └── +page.server.ts
├── products/
│ ├── +page.svelte # /products
│ └── [...rest]/ # Catch-all route
│ └── +page.svelte # /products/*
└── (auth)/ # Route group (nie wpływa na URL)
├── login/
│ └── +page.svelte # /login
└── register/
└── +page.svelte # /registerPliki specjalne
| Plik | Opis |
|---|---|
+page.svelte | Komponent strony |
+page.ts | Universal load function |
+page.server.ts | Server-only load function |
+layout.svelte | Layout wrapper |
+layout.ts | Layout load function |
+layout.server.ts | Server-only layout load |
+server.ts | API endpoint |
+error.svelte | Error boundary |
Dynamic Routes
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
export let data // Z load function
</script>
<article>
<h1>{data.post.title}</h1>
<div class="content">
{@html data.post.content}
</div>
<p>Autor: {data.post.author}</p>
</article>// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
if (!post) {
throw error(404, {
message: 'Post nie został znaleziony'
})
}
return { post }
}Rest Parameters (Catch-all)
// src/routes/docs/[...path]/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params }) => {
// params.path = "getting-started/installation"
// dla URL: /docs/getting-started/installation
const segments = params.path.split('/')
const doc = await fetchDoc(segments)
return { doc, breadcrumbs: segments }
}Optional Parameters
// src/routes/[[lang]]/about/+page.server.ts
// Pasuje do /about i /pl/about i /en/about
export const load: PageServerLoad = async ({ params }) => {
const lang = params.lang || 'pl'
return { lang }
}Route Groups
Route groups (nazwa) organizują kod bez wpływania na URL:
src/routes/
├── (marketing)/ # Grupa marketingowa
│ ├── +layout.svelte # Layout dla marketing
│ ├── pricing/
│ │ └── +page.svelte # /pricing
│ └── features/
│ └── +page.svelte # /features
├── (app)/ # Grupa aplikacji
│ ├── +layout.svelte # Layout dla app
│ ├── dashboard/
│ │ └── +page.svelte # /dashboard
│ └── settings/
│ └── +page.svelte # /settingsData Loading
Universal Load (runs on server & client)
// src/routes/blog/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch, params, url }) => {
// Używaj fetch - SvelteKit go deduplikuje
const response = await fetch('/api/posts')
const posts = await response.json()
// Query parameters
const page = url.searchParams.get('page') || '1'
return {
posts,
page: parseInt(page)
}
}Server Load (runs only on server)
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/database'
import { error, redirect } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, cookies, locals }) => {
// Dostęp do danych serwera
const session = cookies.get('session')
if (!session) {
throw redirect(303, '/login')
}
// Dostęp do bazy danych
const posts = await db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
// Dane z locals (middleware)
const user = locals.user
return { posts, user }
}Layout Load
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies }) => {
const theme = cookies.get('theme') || 'light'
return { theme }
}<!-- src/routes/+layout.svelte -->
<script>
export let data
</script>
<div class="app" data-theme={data.theme}>
<nav>Navigation</nav>
<slot /> <!-- Treść strony -->
<footer>Footer</footer>
</div>Parent Data
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ parent }) => {
// Pobierz dane z parent layout
const { user } = await parent()
const stats = await fetchUserStats(user.id)
return { stats }
}Invalidation i Reloading
<script>
import { invalidate, invalidateAll } from '$app/navigation'
async function refreshPosts() {
// Odśwież konkretny endpoint
await invalidate('/api/posts')
// Lub odśwież wszystko
await invalidateAll()
}
</script>
<button on:click={refreshPosts}>
Odśwież posty
</button>Form Actions
SvelteKit oferuje natywne formularze z progressive enhancement - działają bez JavaScript!
Podstawowe Form Actions
// src/routes/contact/+page.server.ts
import type { Actions } from './$types'
import { fail } from '@sveltejs/kit'
export const actions: Actions = {
// Default action (method="POST")
default: async ({ request }) => {
const data = await request.formData()
const email = data.get('email')
const message = data.get('message')
// Walidacja
if (!email || !message) {
return fail(400, {
error: 'Wszystkie pola są wymagane',
email,
message
})
}
// Zapisz w bazie
await db.contact.create({
data: { email, message }
})
return { success: true }
}
}<!-- src/routes/contact/+page.svelte -->
<script>
export let form // Dane zwrócone z action
</script>
{#if form?.success}
<p class="success">Wiadomość wysłana!</p>
{/if}
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST">
<label>
Email:
<input
type="email"
name="email"
value={form?.email ?? ''}
required
/>
</label>
<label>
Wiadomość:
<textarea
name="message"
required
>{form?.message ?? ''}</textarea>
</label>
<button type="submit">Wyślij</button>
</form>Named Actions
// src/routes/posts/+page.server.ts
import type { Actions } from './$types'
export const actions: Actions = {
create: async ({ request }) => {
const data = await request.formData()
const title = data.get('title')
await db.post.create({ data: { title } })
return { created: true }
},
delete: async ({ request }) => {
const data = await request.formData()
const id = data.get('id')
await db.post.delete({ where: { id } })
return { deleted: true }
},
update: async ({ request }) => {
const data = await request.formData()
const id = data.get('id')
const title = data.get('title')
await db.post.update({
where: { id },
data: { title }
})
return { updated: true }
}
}<!-- src/routes/posts/+page.svelte -->
<script>
export let data
</script>
<!-- Create form - named action -->
<form method="POST" action="?/create">
<input name="title" placeholder="Tytuł posta" required />
<button>Dodaj post</button>
</form>
<!-- Lista postów -->
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<!-- Delete form -->
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={post.id} />
<button>Usuń</button>
</form>
<!-- Update form -->
<form method="POST" action="?/update">
<input type="hidden" name="id" value={post.id} />
<input name="title" value={post.title} />
<button>Aktualizuj</button>
</form>
</article>
{/each}Progressive Enhancement
<script>
import { enhance } from '$app/forms'
let loading = false
</script>
<form
method="POST"
action="?/create"
use:enhance={() => {
loading = true
return async ({ result, update }) => {
loading = false
if (result.type === 'success') {
// Custom behavior
await update() // Domyślne zachowanie
}
}
}}
>
<input name="title" disabled={loading} />
<button disabled={loading}>
{loading ? 'Dodawanie...' : 'Dodaj'}
</button>
</form>API Endpoints
Podstawowe Endpoints
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ url }) => {
const limit = parseInt(url.searchParams.get('limit') || '10')
const users = await db.user.findMany({ take: limit })
return json(users)
}
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
// Walidacja
if (!data.email || !data.name) {
throw error(400, 'Email i nazwa są wymagane')
}
const user = await db.user.create({ data })
return json(user, { status: 201 })
}Dynamic API Routes
// src/routes/api/users/[id]/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ params }) => {
const user = await db.user.findUnique({
where: { id: params.id }
})
if (!user) {
throw error(404, 'Użytkownik nie znaleziony')
}
return json(user)
}
export const PUT: RequestHandler = async ({ params, request }) => {
const data = await request.json()
const user = await db.user.update({
where: { id: params.id },
data
})
return json(user)
}
export const DELETE: RequestHandler = async ({ params }) => {
await db.user.delete({
where: { id: params.id }
})
return new Response(null, { status: 204 })
}Streaming Responses
// src/routes/api/stream/+server.ts
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 500))
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`)
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
}Pliki i Upload
// src/routes/api/upload/+server.ts
import { json, error } from '@sveltejs/kit'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
throw error(400, 'Brak pliku')
}
// Walidacja typu
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
throw error(400, 'Niedozwolony typ pliku')
}
// Zapisz plik
const buffer = Buffer.from(await file.arrayBuffer())
const filename = `${Date.now()}-${file.name}`
const path = join('static', 'uploads', filename)
await writeFile(path, buffer)
return json({ url: `/uploads/${filename}` })
}Svelte 5 Runes
Svelte 5 wprowadza Runes - nowy system reaktywności:
$state - Reactive State
<script>
// Svelte 5 - Runes
let count = $state(0)
let user = $state({ name: 'Jan', age: 25 })
function increment() {
count++ // Automatycznie reaktywne
}
function updateUser() {
user.age++ // Deep reactivity
}
</script>
<p>Count: {count}</p>
<p>User: {user.name}, {user.age} lat</p>
<button onclick={increment}>+1</button>
<button onclick={updateUser}>Urodziny</button>$derived - Computed Values
<script>
let count = $state(0)
// Automatycznie aktualizowane
let double = $derived(count * 2)
let isEven = $derived(count % 2 === 0)
// Dla złożonych obliczeń
let summary = $derived.by(() => {
if (count === 0) return 'Zero'
if (count < 10) return 'Mało'
return 'Dużo'
})
</script>
<p>Count: {count}</p>
<p>Double: {double}</p>
<p>Even: {isEven ? 'Tak' : 'Nie'}</p>
<p>Summary: {summary}</p>
<button onclick={() => count++}>+1</button>$effect - Side Effects
<script>
let count = $state(0)
let savedCount = $state(0)
// Uruchamia się gdy count się zmieni
$effect(() => {
console.log(`Count changed to ${count}`)
// Cleanup (opcjonalnie)
return () => {
console.log('Cleanup')
}
})
// Effect z warunkiem
$effect(() => {
if (count > 10) {
savedCount = count
}
})
// Pre-effect (przed DOM update)
$effect.pre(() => {
// Uruchamia się przed aktualizacją DOM
})
</script>$props - Component Props
<!-- Child.svelte -->
<script>
// Svelte 5 - $props
let {
name,
age = 18, // Default value
onUpdate,
children // Slot content
} = $props()
</script>
<div>
<h2>{name}, {age} lat</h2>
<button onclick={() => onUpdate?.(age + 1)}>
Urodziny
</button>
{@render children?.()}
</div><!-- Parent.svelte -->
<script>
import Child from './Child.svelte'
let age = $state(25)
</script>
<Child
name="Anna"
{age}
onUpdate={(newAge) => age = newAge}
>
<p>To jest treść przekazana do child</p>
</Child>$bindable - Two-way Binding
<!-- Input.svelte -->
<script>
let { value = $bindable() } = $props()
</script>
<input bind:value /><!-- Parent.svelte -->
<script>
import Input from './Input.svelte'
let text = $state('')
</script>
<Input bind:value={text} />
<p>Wpisany tekst: {text}</p>Hooks i Middleware
Server Hooks
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit'
// Middleware dla wszystkich requestów
export const handle: Handle = async ({ event, resolve }) => {
// Przed przetwarzaniem
const session = event.cookies.get('session')
if (session) {
const user = await getUserFromSession(session)
event.locals.user = user
}
// Przetwórz request
const response = await resolve(event, {
// Opcje transformacji HTML
transformPageChunk: ({ html }) => html.replace(
'%theme%',
event.cookies.get('theme') || 'light'
)
})
// Po przetwarzaniu
response.headers.set('X-Custom-Header', 'value')
return response
}
// Przechwytywanie fetch na serwerze
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
// Dodaj nagłówki do wewnętrznych requestów
if (request.url.startsWith('https://api.internal.com')) {
request.headers.set('Authorization', `Bearer ${API_KEY}`)
}
return fetch(request)
}
// Obsługa błędów
export const handleServerError: HandleServerError = async ({ error, event }) => {
console.error('Server error:', error, 'URL:', event.url)
// Logowanie do zewnętrznego serwisu
await logError(error)
return {
message: 'Wystąpił błąd serwera'
}
}Client Hooks
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit'
export const handleClientError: HandleClientError = async ({ error, message }) => {
console.error('Client error:', error)
// Logowanie do analytics
await trackError(error)
return {
message: 'Coś poszło nie tak'
}
}Auth Middleware Pattern
// src/hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit'
const publicRoutes = ['/', '/login', '/register', '/about']
export const handle: Handle = async ({ event, resolve }) => {
// Sprawdź sesję
const session = event.cookies.get('session')
if (session) {
try {
const user = await verifySession(session)
event.locals.user = user
} catch {
// Sesja nieważna
event.cookies.delete('session', { path: '/' })
}
}
// Sprawdź autoryzację
const isPublic = publicRoutes.some(route =>
event.url.pathname === route ||
event.url.pathname.startsWith('/api/public')
)
if (!isPublic && !event.locals.user) {
throw redirect(303, '/login')
}
return resolve(event)
}Prerendering i SSG
Konfiguracja Prerendering
// src/routes/blog/+page.ts
export const prerender = true // Prerender tę stronę
// src/routes/admin/+page.ts
export const prerender = false // Nie prerenderuj (wymaga auth)Dynamiczne Prerendering
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad, EntryGenerator } from './$types'
// Definiuj które strony prerender
export const entries: EntryGenerator = async () => {
const posts = await db.post.findMany({
select: { slug: true }
})
return posts.map(post => ({ slug: post.slug }))
}
export const prerender = true
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
return { post }
}SSG + ISR Pattern
// svelte.config.js
const config = {
kit: {
adapter: adapter(),
prerender: {
// Prerender wszystkie strony
entries: ['*'],
// Jak obsługiwać błędy
handleHttpError: ({ path, referrer, message }) => {
if (path.startsWith('/api/')) {
return // Ignoruj API routes
}
throw new Error(message)
}
}
}
}Deployment Adapters
Adapter Auto (wykrywa automatycznie)
npm install @sveltejs/adapter-auto// svelte.config.js
import adapter from '@sveltejs/adapter-auto'
const config = {
kit: {
adapter: adapter()
}
}Adapter Node.js
npm install @sveltejs/adapter-nodeimport adapter from '@sveltejs/adapter-node'
const config = {
kit: {
adapter: adapter({
out: 'build',
precompress: true,
envPrefix: 'MY_APP_'
})
}
}Adapter Static (SSG)
npm install @sveltejs/adapter-staticimport adapter from '@sveltejs/adapter-static'
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
precompress: false
})
}
}Adapter Vercel
npm install @sveltejs/adapter-vercelimport adapter from '@sveltejs/adapter-vercel'
const config = {
kit: {
adapter: adapter({
runtime: 'edge', // lub 'nodejs18.x'
regions: ['fra1'],
split: true // Funkcje per-route
})
}
}Adapter Cloudflare
npm install @sveltejs/adapter-cloudflareimport adapter from '@sveltejs/adapter-cloudflare'
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
}Integracje
Tailwind CSS
npx sv add tailwindcss<!-- +page.svelte -->
<script>
let count = $state(0)
</script>
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg">
<h1 class="text-2xl font-bold text-gray-800 mb-4">
Counter: {count}
</h1>
<button
onclick={() => count++}
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
transition-colors"
>
Increment
</button>
</div>
</div>Prisma ORM
npm install prisma @prisma/client
npx prisma init// src/lib/server/database.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const db = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db
}Auth.js (NextAuth dla SvelteKit)
npm install @auth/sveltekit @auth/core// src/hooks.server.ts
import { SvelteKitAuth } from '@auth/sveltekit'
import GitHub from '@auth/sveltekit/providers/github'
export const { handle, signIn, signOut } = SvelteKitAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
})
]
})<!-- src/routes/+page.svelte -->
<script>
import { page } from '$app/stores'
import { signIn, signOut } from '@auth/sveltekit/client'
</script>
{#if $page.data.session}
<p>Zalogowany jako {$page.data.session.user?.email}</p>
<button onclick={() => signOut()}>Wyloguj</button>
{:else}
<button onclick={() => signIn('github')}>
Zaloguj przez GitHub
</button>
{/if}Superforms
npm install sveltekit-superforms zod// src/routes/contact/+page.server.ts
import { superValidate, message } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Nieprawidłowy email'),
message: z.string().min(10, 'Minimum 10 znaków')
})
export const load = async () => {
const form = await superValidate(zod(schema))
return { form }
}
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod(schema))
if (!form.valid) {
return { form }
}
// Wyślij email...
return message(form, 'Wysłano!')
}
}<!-- src/routes/contact/+page.svelte -->
<script>
import { superForm } from 'sveltekit-superforms'
export let data
const { form, errors, message, enhance } = superForm(data.form)
</script>
{#if $message}
<p class="success">{$message}</p>
{/if}
<form method="POST" use:enhance>
<label>
Email:
<input type="email" name="email" bind:value={$form.email} />
{#if $errors.email}
<span class="error">{$errors.email}</span>
{/if}
</label>
<label>
Wiadomość:
<textarea name="message" bind:value={$form.message}></textarea>
{#if $errors.message}
<span class="error">{$errors.message}</span>
{/if}
</label>
<button type="submit">Wyślij</button>
</form>Stores i State Management
Svelte Stores
// src/lib/stores/cart.ts
import { writable, derived } from 'svelte/store'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
function createCartStore() {
const { subscribe, set, update } = writable<CartItem[]>([])
return {
subscribe,
addItem: (item: Omit<CartItem, 'quantity'>) => {
update(items => {
const existing = items.find(i => i.id === item.id)
if (existing) {
existing.quantity++
return [...items]
}
return [...items, { ...item, quantity: 1 }]
})
},
removeItem: (id: string) => {
update(items => items.filter(i => i.id !== id))
},
clear: () => set([])
}
}
export const cart = createCartStore()
// Derived store dla sumy
export const cartTotal = derived(cart, $cart =>
$cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
)<script>
import { cart, cartTotal } from '$lib/stores/cart'
</script>
<p>Suma: {$cartTotal} PLN</p>
{#each $cart as item}
<div>
{item.name} x{item.quantity}
<button onclick={() => cart.removeItem(item.id)}>
Usuń
</button>
</div>
{/each}Context API
<!-- src/routes/+layout.svelte -->
<script>
import { setContext } from 'svelte'
const theme = $state({ mode: 'light', accent: 'blue' })
setContext('theme', {
get current() { return theme },
toggle: () => theme.mode = theme.mode === 'light' ? 'dark' : 'light'
})
</script>
<slot /><!-- Dowolny komponent potomny -->
<script>
import { getContext } from 'svelte'
const { current, toggle } = getContext('theme')
</script>
<button onclick={toggle}>
Motyw: {current.mode}
</button>Testowanie
Vitest dla Unit Tests
npx sv add vitest// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice, calculateDiscount } from './utils'
describe('formatPrice', () => {
it('formats price with currency', () => {
expect(formatPrice(1234.56)).toBe('1 234,56 PLN')
})
it('handles zero', () => {
expect(formatPrice(0)).toBe('0,00 PLN')
})
})
describe('calculateDiscount', () => {
it('calculates percentage discount', () => {
expect(calculateDiscount(100, 20)).toBe(80)
})
})Playwright dla E2E
npx sv add playwright// tests/home.test.ts
import { expect, test } from '@playwright/test'
test('homepage has correct title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/Home/)
})
test('can navigate to about page', async ({ page }) => {
await page.goto('/')
await page.click('text=O nas')
await expect(page.url()).toContain('/about')
})
test('contact form submits successfully', async ({ page }) => {
await page.goto('/contact')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('textarea[name="message"]', 'Test message')
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})Testing Library
npm install @testing-library/svelte// src/lib/components/Counter.test.ts
import { render, fireEvent } from '@testing-library/svelte'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.svelte'
describe('Counter', () => {
it('renders initial count', () => {
const { getByText } = render(Counter, { props: { initial: 5 } })
expect(getByText('Count: 5')).toBeInTheDocument()
})
it('increments on click', async () => {
const { getByText, getByRole } = render(Counter)
await fireEvent.click(getByRole('button', { name: '+1' }))
expect(getByText('Count: 1')).toBeInTheDocument()
})
})Performance Optimization
Lazy Loading
<script>
import { onMount } from 'svelte'
let HeavyComponent = $state()
onMount(async () => {
const module = await import('./HeavyComponent.svelte')
HeavyComponent = module.default
})
</script>
{#if HeavyComponent}
<svelte:component this={HeavyComponent} />
{:else}
<p>Ładowanie...</p>
{/if}Streaming
// src/routes/dashboard/+page.server.ts
export const load = async () => {
return {
// Natychmiast
user: await getUser(),
// Streamowane (nie blokuje renderowania)
stats: getStats(), // Promise bez await
notifications: getNotifications()
}
}<script>
export let data
</script>
<h1>Witaj, {data.user.name}</h1>
{#await data.stats}
<p>Ładowanie statystyk...</p>
{:then stats}
<Stats {stats} />
{/await}
{#await data.notifications}
<p>Ładowanie powiadomień...</p>
{:then notifications}
<Notifications {notifications} />
{/await}Service Worker
// src/service-worker.ts
import { build, files, version } from '$service-worker'
const CACHE = `cache-${version}`
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll([...build, ...files]))
)
})
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request)
})
)
})FAQ - Najczęściej Zadawane Pytania
Czy SvelteKit nadaje się do dużych projektów?
Tak! SvelteKit skaluje się doskonale dzięki modularnej architekturze i małym bundle'om. Jest używany w produkcji przez firmy takie jak The New York Times, Spotify i Apple.
Jaka jest różnica między Svelte a SvelteKit?
Svelte to komponent framework (jak React/Vue), a SvelteKit to fullstack meta-framework zbudowany na Svelte (jak Next.js dla React). SvelteKit dodaje routing, SSR, form actions i API endpoints.
Czy mogę używać komponentów React w SvelteKit?
Nie bezpośrednio - Svelte i React mają różne modele komponentów. Możesz jednak integrować widgety React przez portale lub iframe'y.
Jak działa reaktywność w Svelte 5?
Svelte 5 wprowadza Runes - $state, $derived, $effect - które są bardziej eksplicytne i przewidywalne niż poprzedni system oparty na $: i let. Runes działają podobnie do React hooks, ale bez zasad hooks.
Czy SvelteKit wspiera ISR (Incremental Static Regeneration)?
SvelteKit nie ma wbudowanego ISR jak Next.js, ale możesz osiągnąć podobny efekt przez adapter Vercel z isr opcją lub przez revalidation w Cloudflare.
Jak zabezpieczyć API endpoints?
Używaj hooks.server.ts do middleware auth, sprawdzaj event.locals.user w endpointach i używaj form actions z CSRF protection (wbudowane w SvelteKit).
Czy SvelteKit wspiera Edge Functions?
Tak! Adaptery Vercel i Cloudflare wspierają edge runtime. Ustaw runtime: 'edge' w konfiguracji adaptera.
Podsumowanie
SvelteKit to nowoczesny fullstack framework, który łączy elegancję Svelte z potężnymi możliwościami serwerowymi:
- Brak runtime - najmniejsze możliwe bundle
- Svelte 5 Runes - nowoczesny system reaktywności
- Form Actions - natywne formularze z progressive enhancement
- File-based routing - intuicyjna struktura projektu
- Uniwersalny - SSR, SSG, SPA w jednym frameworku
- Świetny DX - Vite, TypeScript, HMR
Idealny dla zespołów ceniących prostotę, wydajność i nowoczesne podejście do web developmentu.
SvelteKit - a complete guide to the Svelte fullstack framework
What is SvelteKit?
SvelteKit is the official fullstack framework for Svelte, designed for building modern web applications. It combines the elegance and simplicity of Svelte with powerful server-side capabilities - SSR, file-based routing, form actions, and API endpoints. All of this with minimal boilerplate and incredible performance thanks to its unique compiler-based approach.
Unlike other frameworks, Svelte compiles components into native JavaScript at build time, eliminating the need to include a runtime in the bundle. This means smaller files, faster loading, and better performance. SvelteKit builds on this architecture and adds routing, data loading, form handling, and deployment adapters.
Why SvelteKit?
Key advantages of SvelteKit
- No runtime - The compiler generates native JavaScript
- Fastest framework - Smaller bundles, faster hydration
- Simple mental model - Intuitive syntax with no boilerplate
- File-based routing - Folder structure = URL structure
- Form actions - Native forms with progressive enhancement
- Universal - SSR, SSG, SPA, all in one
- Svelte 5 Runes - Modern reactivity system
- Vite under the hood - Lightning-fast dev server
SvelteKit vs other frameworks
| Feature | SvelteKit | Next.js | Nuxt | Remix |
|---|---|---|---|---|
| Runtime | None | React | Vue | React |
| Bundle size | Smallest | Medium | Medium | Small |
| Learning curve | Easy | Medium | Medium | Medium |
| Form handling | Native | Client-side | Client-side | Native |
| Compiler | Yes | No | No | No |
| Dev experience | Excellent | Good | Good | Good |
| TypeScript | Full | Full | Full | Full |
| Deploy adapters | Many | Vercel-first | Many | Many |
When to choose SvelteKit?
- Smaller teams - simpler syntax, less code
- Performance-critical - smallest bundles
- Progressive enhancement - native forms
- Fast development - HMR via Vite
- New projects - modern architecture
Installation and configuration
Creating a new project
npx sv create my-app
# The interactive wizard will ask about:
# - Template (skeleton, demo app, library)
# - TypeScript (yes/no)
# - Add-ons (Tailwind, ESLint, Prettier, Playwright, Vitest)
cd my-app
npm install
npm run devAlternative methods
# With pnpm
pnpm create svelte@latest my-app
# With yarn
yarn create svelte my-app
# With bunx
bunx sv create my-appProject structure
my-app/
├── src/
│ ├── app.html # HTML template
│ ├── app.css # Global styles
│ ├── app.d.ts # TypeScript declarations
│ ├── lib/ # Shared code ($lib alias)
│ │ ├── components/ # Components
│ │ ├── server/ # Server-only code
│ │ └── utils/ # Helpers
│ └── routes/ # File-based routing
│ ├── +page.svelte # Home page
│ ├── +page.server.ts # Server load
│ ├── +layout.svelte # Layout
│ ├── +error.svelte # Error page
│ └── api/ # API endpoints
│ └── +server.ts
├── static/ # Static assets
├── svelte.config.js # Svelte config
├── vite.config.ts # Vite config
├── tsconfig.json
└── package.jsonConfiguring svelte.config.js
// 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: {
$components: 'src/lib/components',
$utils: 'src/lib/utils'
},
csp: {
mode: 'auto',
directives: {
'script-src': ['self']
}
},
prerender: {
handleHttpError: 'warn'
}
}
}
export default configFile-based routing
Basic structure
SvelteKit uses the folder structure inside src/routes/ to define routing:
src/routes/
├── +page.svelte # /
├── +layout.svelte # Layout for all pages
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ ├── +page.server.ts # Data loading
│ └── [slug]/ # Dynamic route
│ ├── +page.svelte # /blog/:slug
│ └── +page.server.ts
├── products/
│ ├── +page.svelte # /products
│ └── [...rest]/ # Catch-all route
│ └── +page.svelte # /products/*
└── (auth)/ # Route group (does not affect URL)
├── login/
│ └── +page.svelte # /login
└── register/
└── +page.svelte # /registerSpecial files
| File | Description |
|---|---|
+page.svelte | Page component |
+page.ts | Universal load function |
+page.server.ts | Server-only load function |
+layout.svelte | Layout wrapper |
+layout.ts | Layout load function |
+layout.server.ts | Server-only layout load |
+server.ts | API endpoint |
+error.svelte | Error boundary |
Dynamic routes
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
export let data
</script>
<article>
<h1>{data.post.title}</h1>
<div class="content">
{@html data.post.content}
</div>
<p>Author: {data.post.author}</p>
</article>// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
if (!post) {
throw error(404, {
message: 'Post not found'
})
}
return { post }
}Rest parameters (catch-all)
// src/routes/docs/[...path]/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params }) => {
// params.path = "getting-started/installation"
// for URL: /docs/getting-started/installation
const segments = params.path.split('/')
const doc = await fetchDoc(segments)
return { doc, breadcrumbs: segments }
}Optional parameters
// src/routes/[[lang]]/about/+page.server.ts
// Matches /about and /pl/about and /en/about
export const load: PageServerLoad = async ({ params }) => {
const lang = params.lang || 'pl'
return { lang }
}Route groups
Route groups (name) organize code without affecting the URL:
src/routes/
├── (marketing)/ # Marketing group
│ ├── +layout.svelte # Layout for marketing
│ ├── pricing/
│ │ └── +page.svelte # /pricing
│ └── features/
│ └── +page.svelte # /features
├── (app)/ # App group
│ ├── +layout.svelte # Layout for app
│ ├── dashboard/
│ │ └── +page.svelte # /dashboard
│ └── settings/
│ └── +page.svelte # /settingsData loading
Universal load (runs on server and client)
// src/routes/blog/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch, params, url }) => {
// Use fetch - SvelteKit deduplicates it
const response = await fetch('/api/posts')
const posts = await response.json()
// Query parameters
const page = url.searchParams.get('page') || '1'
return {
posts,
page: parseInt(page)
}
}Server load (runs only on server)
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/database'
import { error, redirect } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, cookies, locals }) => {
const session = cookies.get('session')
if (!session) {
throw redirect(303, '/login')
}
const posts = await db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
})
const user = locals.user
return { posts, user }
}Layout load
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ cookies }) => {
const theme = cookies.get('theme') || 'light'
return { theme }
}<!-- src/routes/+layout.svelte -->
<script>
export let data
</script>
<div class="app" data-theme={data.theme}>
<nav>Navigation</nav>
<slot />
<footer>Footer</footer>
</div>Parent data
// src/routes/dashboard/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ parent }) => {
// Get data from the parent layout
const { user } = await parent()
const stats = await fetchUserStats(user.id)
return { stats }
}Invalidation and reloading
<script>
import { invalidate, invalidateAll } from '$app/navigation'
async function refreshPosts() {
// Refresh a specific endpoint
await invalidate('/api/posts')
// Or refresh everything
await invalidateAll()
}
</script>
<button on:click={refreshPosts}>
Refresh posts
</button>Form actions
SvelteKit offers native forms with progressive enhancement - they work without JavaScript!
Basic form actions
// src/routes/contact/+page.server.ts
import type { Actions } from './$types'
import { fail } from '@sveltejs/kit'
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = data.get('email')
const message = data.get('message')
if (!email || !message) {
return fail(400, {
error: 'All fields are required',
email,
message
})
}
await db.contact.create({
data: { email, message }
})
return { success: true }
}
}<!-- src/routes/contact/+page.svelte -->
<script>
export let form
</script>
{#if form?.success}
<p class="success">Message sent!</p>
{/if}
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST">
<label>
Email:
<input
type="email"
name="email"
value={form?.email ?? ''}
required
/>
</label>
<label>
Message:
<textarea
name="message"
required
>{form?.message ?? ''}</textarea>
</label>
<button type="submit">Send</button>
</form>Named actions
// src/routes/posts/+page.server.ts
import type { Actions } from './$types'
export const actions: Actions = {
create: async ({ request }) => {
const data = await request.formData()
const title = data.get('title')
await db.post.create({ data: { title } })
return { created: true }
},
delete: async ({ request }) => {
const data = await request.formData()
const id = data.get('id')
await db.post.delete({ where: { id } })
return { deleted: true }
},
update: async ({ request }) => {
const data = await request.formData()
const id = data.get('id')
const title = data.get('title')
await db.post.update({
where: { id },
data: { title }
})
return { updated: true }
}
}<!-- src/routes/posts/+page.svelte -->
<script>
export let data
</script>
<!-- Create form - named action -->
<form method="POST" action="?/create">
<input name="title" placeholder="Post title" required />
<button>Add post</button>
</form>
<!-- Post list -->
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<!-- Delete form -->
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={post.id} />
<button>Delete</button>
</form>
<!-- Update form -->
<form method="POST" action="?/update">
<input type="hidden" name="id" value={post.id} />
<input name="title" value={post.title} />
<button>Update</button>
</form>
</article>
{/each}Progressive enhancement
<script>
import { enhance } from '$app/forms'
let loading = false
</script>
<form
method="POST"
action="?/create"
use:enhance={() => {
loading = true
return async ({ result, update }) => {
loading = false
if (result.type === 'success') {
await update()
}
}
}}
>
<input name="title" disabled={loading} />
<button disabled={loading}>
{loading ? 'Adding...' : 'Add'}
</button>
</form>API endpoints
Basic endpoints
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ url }) => {
const limit = parseInt(url.searchParams.get('limit') || '10')
const users = await db.user.findMany({ take: limit })
return json(users)
}
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
if (!data.email || !data.name) {
throw error(400, 'Email and name are required')
}
const user = await db.user.create({ data })
return json(user, { status: 201 })
}Dynamic API routes
// src/routes/api/users/[id]/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ params }) => {
const user = await db.user.findUnique({
where: { id: params.id }
})
if (!user) {
throw error(404, 'User not found')
}
return json(user)
}
export const PUT: RequestHandler = async ({ params, request }) => {
const data = await request.json()
const user = await db.user.update({
where: { id: params.id },
data
})
return json(user)
}
export const DELETE: RequestHandler = async ({ params }) => {
await db.user.delete({
where: { id: params.id }
})
return new Response(null, { status: 204 })
}Streaming responses
// src/routes/api/stream/+server.ts
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 500))
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`)
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
}Files and upload
// src/routes/api/upload/+server.ts
import { json, error } from '@sveltejs/kit'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request }) => {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
throw error(400, 'No file provided')
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
throw error(400, 'File type not allowed')
}
const buffer = Buffer.from(await file.arrayBuffer())
const filename = `${Date.now()}-${file.name}`
const path = join('static', 'uploads', filename)
await writeFile(path, buffer)
return json({ url: `/uploads/${filename}` })
}Svelte 5 Runes
Svelte 5 introduces Runes - a new reactivity system:
$state - reactive state
<script>
let count = $state(0)
let user = $state({ name: 'Jan', age: 25 })
function increment() {
count++
}
function updateUser() {
user.age++
}
</script>
<p>Count: {count}</p>
<p>User: {user.name}, {user.age} years old</p>
<button onclick={increment}>+1</button>
<button onclick={updateUser}>Birthday</button>$derived - computed values
<script>
let count = $state(0)
let double = $derived(count * 2)
let isEven = $derived(count % 2 === 0)
let summary = $derived.by(() => {
if (count === 0) return 'Zero'
if (count < 10) return 'Few'
return 'Many'
})
</script>
<p>Count: {count}</p>
<p>Double: {double}</p>
<p>Even: {isEven ? 'Yes' : 'No'}</p>
<p>Summary: {summary}</p>
<button onclick={() => count++}>+1</button>$effect - side effects
<script>
let count = $state(0)
let savedCount = $state(0)
$effect(() => {
console.log(`Count changed to ${count}`)
return () => {
console.log('Cleanup')
}
})
$effect(() => {
if (count > 10) {
savedCount = count
}
})
$effect.pre(() => {
// Runs before DOM update
})
</script>$props - component props
<!-- Child.svelte -->
<script>
let {
name,
age = 18,
onUpdate,
children
} = $props()
</script>
<div>
<h2>{name}, {age} years old</h2>
<button onclick={() => onUpdate?.(age + 1)}>
Birthday
</button>
{@render children?.()}
</div><!-- Parent.svelte -->
<script>
import Child from './Child.svelte'
let age = $state(25)
</script>
<Child
name="Anna"
{age}
onUpdate={(newAge) => age = newAge}
>
<p>This is content passed to the child</p>
</Child>$bindable - two-way binding
<!-- Input.svelte -->
<script>
let { value = $bindable() } = $props()
</script>
<input bind:value /><!-- Parent.svelte -->
<script>
import Input from './Input.svelte'
let text = $state('')
</script>
<Input bind:value={text} />
<p>Typed text: {text}</p>Hooks and middleware
Server hooks
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit'
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session')
if (session) {
const user = await getUserFromSession(session)
event.locals.user = user
}
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace(
'%theme%',
event.cookies.get('theme') || 'light'
)
})
response.headers.set('X-Custom-Header', 'value')
return response
}
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
if (request.url.startsWith('https://api.internal.com')) {
request.headers.set('Authorization', `Bearer ${API_KEY}`)
}
return fetch(request)
}
export const handleServerError: HandleServerError = async ({ error, event }) => {
console.error('Server error:', error, 'URL:', event.url)
await logError(error)
return {
message: 'A server error occurred'
}
}Client hooks
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit'
export const handleClientError: HandleClientError = async ({ error, message }) => {
console.error('Client error:', error)
await trackError(error)
return {
message: 'Something went wrong'
}
}Auth middleware pattern
// src/hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit'
const publicRoutes = ['/', '/login', '/register', '/about']
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session')
if (session) {
try {
const user = await verifySession(session)
event.locals.user = user
} catch {
event.cookies.delete('session', { path: '/' })
}
}
const isPublic = publicRoutes.some(route =>
event.url.pathname === route ||
event.url.pathname.startsWith('/api/public')
)
if (!isPublic && !event.locals.user) {
throw redirect(303, '/login')
}
return resolve(event)
}Prerendering and SSG
Prerendering configuration
// src/routes/blog/+page.ts
export const prerender = true
// src/routes/admin/+page.ts
export const prerender = falseDynamic prerendering
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad, EntryGenerator } from './$types'
export const entries: EntryGenerator = async () => {
const posts = await db.post.findMany({
select: { slug: true }
})
return posts.map(post => ({ slug: post.slug }))
}
export const prerender = true
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
return { post }
}SSG + ISR pattern
// svelte.config.js
const config = {
kit: {
adapter: adapter(),
prerender: {
entries: ['*'],
handleHttpError: ({ path, referrer, message }) => {
if (path.startsWith('/api/')) {
return
}
throw new Error(message)
}
}
}
}Deployment adapters
Adapter Auto (auto-detection)
npm install @sveltejs/adapter-auto// svelte.config.js
import adapter from '@sveltejs/adapter-auto'
const config = {
kit: {
adapter: adapter()
}
}Adapter Node.js
npm install @sveltejs/adapter-nodeimport adapter from '@sveltejs/adapter-node'
const config = {
kit: {
adapter: adapter({
out: 'build',
precompress: true,
envPrefix: 'MY_APP_'
})
}
}Adapter Static (SSG)
npm install @sveltejs/adapter-staticimport adapter from '@sveltejs/adapter-static'
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
precompress: false
})
}
}Adapter Vercel
npm install @sveltejs/adapter-vercelimport adapter from '@sveltejs/adapter-vercel'
const config = {
kit: {
adapter: adapter({
runtime: 'edge',
regions: ['fra1'],
split: true
})
}
}Adapter Cloudflare
npm install @sveltejs/adapter-cloudflareimport adapter from '@sveltejs/adapter-cloudflare'
const config = {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
}Integrations
Tailwind CSS
npx sv add tailwindcss<!-- +page.svelte -->
<script>
let count = $state(0)
</script>
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg">
<h1 class="text-2xl font-bold text-gray-800 mb-4">
Counter: {count}
</h1>
<button
onclick={() => count++}
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
transition-colors"
>
Increment
</button>
</div>
</div>Prisma ORM
npm install prisma @prisma/client
npx prisma init// src/lib/server/database.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const db = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db
}Auth.js (NextAuth for SvelteKit)
npm install @auth/sveltekit @auth/core// src/hooks.server.ts
import { SvelteKitAuth } from '@auth/sveltekit'
import GitHub from '@auth/sveltekit/providers/github'
export const { handle, signIn, signOut } = SvelteKitAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
})
]
})<!-- src/routes/+page.svelte -->
<script>
import { page } from '$app/stores'
import { signIn, signOut } from '@auth/sveltekit/client'
</script>
{#if $page.data.session}
<p>Logged in as {$page.data.session.user?.email}</p>
<button onclick={() => signOut()}>Sign out</button>
{:else}
<button onclick={() => signIn('github')}>
Sign in with GitHub
</button>
{/if}Superforms
npm install sveltekit-superforms zod// src/routes/contact/+page.server.ts
import { superValidate, message } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Invalid email'),
message: z.string().min(10, 'Minimum 10 characters')
})
export const load = async () => {
const form = await superValidate(zod(schema))
return { form }
}
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod(schema))
if (!form.valid) {
return { form }
}
// Send email...
return message(form, 'Sent!')
}
}<!-- src/routes/contact/+page.svelte -->
<script>
import { superForm } from 'sveltekit-superforms'
export let data
const { form, errors, message, enhance } = superForm(data.form)
</script>
{#if $message}
<p class="success">{$message}</p>
{/if}
<form method="POST" use:enhance>
<label>
Email:
<input type="email" name="email" bind:value={$form.email} />
{#if $errors.email}
<span class="error">{$errors.email}</span>
{/if}
</label>
<label>
Message:
<textarea name="message" bind:value={$form.message}></textarea>
{#if $errors.message}
<span class="error">{$errors.message}</span>
{/if}
</label>
<button type="submit">Send</button>
</form>Stores and state management
Svelte stores
// src/lib/stores/cart.ts
import { writable, derived } from 'svelte/store'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
function createCartStore() {
const { subscribe, set, update } = writable<CartItem[]>([])
return {
subscribe,
addItem: (item: Omit<CartItem, 'quantity'>) => {
update(items => {
const existing = items.find(i => i.id === item.id)
if (existing) {
existing.quantity++
return [...items]
}
return [...items, { ...item, quantity: 1 }]
})
},
removeItem: (id: string) => {
update(items => items.filter(i => i.id !== id))
},
clear: () => set([])
}
}
export const cart = createCartStore()
export const cartTotal = derived(cart, $cart =>
$cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
)<script>
import { cart, cartTotal } from '$lib/stores/cart'
</script>
<p>Total: {$cartTotal} PLN</p>
{#each $cart as item}
<div>
{item.name} x{item.quantity}
<button onclick={() => cart.removeItem(item.id)}>
Remove
</button>
</div>
{/each}Context API
<!-- src/routes/+layout.svelte -->
<script>
import { setContext } from 'svelte'
const theme = $state({ mode: 'light', accent: 'blue' })
setContext('theme', {
get current() { return theme },
toggle: () => theme.mode = theme.mode === 'light' ? 'dark' : 'light'
})
</script>
<slot /><!-- Any child component -->
<script>
import { getContext } from 'svelte'
const { current, toggle } = getContext('theme')
</script>
<button onclick={toggle}>
Theme: {current.mode}
</button>Testing
Vitest for unit tests
npx sv add vitest// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice, calculateDiscount } from './utils'
describe('formatPrice', () => {
it('formats price with currency', () => {
expect(formatPrice(1234.56)).toBe('1 234,56 PLN')
})
it('handles zero', () => {
expect(formatPrice(0)).toBe('0,00 PLN')
})
})
describe('calculateDiscount', () => {
it('calculates percentage discount', () => {
expect(calculateDiscount(100, 20)).toBe(80)
})
})Playwright for E2E
npx sv add playwright// tests/home.test.ts
import { expect, test } from '@playwright/test'
test('homepage has correct title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/Home/)
})
test('can navigate to about page', async ({ page }) => {
await page.goto('/')
await page.click('text=About us')
await expect(page.url()).toContain('/about')
})
test('contact form submits successfully', async ({ page }) => {
await page.goto('/contact')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('textarea[name="message"]', 'Test message')
await page.click('button[type="submit"]')
await expect(page.locator('.success')).toBeVisible()
})Testing Library
npm install @testing-library/svelte// src/lib/components/Counter.test.ts
import { render, fireEvent } from '@testing-library/svelte'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.svelte'
describe('Counter', () => {
it('renders initial count', () => {
const { getByText } = render(Counter, { props: { initial: 5 } })
expect(getByText('Count: 5')).toBeInTheDocument()
})
it('increments on click', async () => {
const { getByText, getByRole } = render(Counter)
await fireEvent.click(getByRole('button', { name: '+1' }))
expect(getByText('Count: 1')).toBeInTheDocument()
})
})Performance optimization
Lazy loading
<script>
import { onMount } from 'svelte'
let HeavyComponent = $state()
onMount(async () => {
const module = await import('./HeavyComponent.svelte')
HeavyComponent = module.default
})
</script>
{#if HeavyComponent}
<svelte:component this={HeavyComponent} />
{:else}
<p>Loading...</p>
{/if}Streaming
// src/routes/dashboard/+page.server.ts
export const load = async () => {
return {
// Immediate
user: await getUser(),
// Streamed (does not block rendering)
stats: getStats(),
notifications: getNotifications()
}
}<script>
export let data
</script>
<h1>Welcome, {data.user.name}</h1>
{#await data.stats}
<p>Loading stats...</p>
{:then stats}
<Stats {stats} />
{/await}
{#await data.notifications}
<p>Loading notifications...</p>
{:then notifications}
<Notifications {notifications} />
{/await}Service worker
// src/service-worker.ts
import { build, files, version } from '$service-worker'
const CACHE = `cache-${version}`
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll([...build, ...files]))
)
})
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request)
})
)
})FAQ - frequently asked questions
Is SvelteKit suitable for large projects?
Yes! SvelteKit scales excellently thanks to its modular architecture and small bundles. It is used in production by companies like The New York Times, Spotify, and Apple.
What is the difference between Svelte and SvelteKit?
Svelte is a component framework (like React/Vue), while SvelteKit is a fullstack meta-framework built on top of Svelte (like Next.js for React). SvelteKit adds routing, SSR, form actions, and API endpoints.
Can I use React components in SvelteKit?
Not directly - Svelte and React have different component models. However, you can integrate React widgets through portals or iframes.
How does reactivity work in Svelte 5?
Svelte 5 introduces Runes - $state, $derived, $effect - which are more explicit and predictable than the previous system based on $: and let. Runes work similarly to React hooks, but without the rules of hooks.
Does SvelteKit support ISR (Incremental Static Regeneration)?
SvelteKit does not have built-in ISR like Next.js, but you can achieve a similar effect through the Vercel adapter with the isr option or through revalidation on Cloudflare.
How to secure API endpoints?
Use hooks.server.ts for auth middleware, check event.locals.user in endpoints, and use form actions with CSRF protection (built into SvelteKit).
Does SvelteKit support Edge Functions?
Yes! The Vercel and Cloudflare adapters support edge runtime. Set runtime: 'edge' in the adapter configuration.
Summary
SvelteKit is a modern fullstack framework that combines the elegance of Svelte with powerful server-side capabilities:
- No runtime - smallest possible bundles
- Svelte 5 Runes - modern reactivity system
- Form Actions - native forms with progressive enhancement
- File-based routing - intuitive project structure
- Universal - SSR, SSG, SPA in one framework
- Excellent DX - Vite, TypeScript, HMR
Ideal for teams that value simplicity, performance, and a modern approach to web development.