SvelteKit, fullstackowy framework aplikacji Svelte
SvelteKit to oficjalny framework do budowania aplikacji w Svelte: routing oparty o strukturę katalogów, ładowanie danych po stronie serwera, obsługa formularzy bez pisania własnego API i wdrożenie na wybraną platformę przez wymienny adapter. Aktualna wersja to 2.70.2, a Svelte, na którym stoi, jest w wydaniu 5.56.8. Oba pakiety na licencji MIT.
Co odróżnia to od reszty frameworków
Podstawowa różnica leży poziom niżej, w samym Svelte, i warto ją nazwać, bo z niej wynika reszta.
Svelte jest kompilatorem, a nie biblioteką dołączaną do aplikacji. Kod komponentu zamienia się przy budowaniu w zwykły JavaScript operujący bezpośrednio na dokumencie, więc do przeglądarki nie trafia warstwa pośrednicząca porównująca drzewa elementów. Skutkiem jest mniejsza paczka i mniej pracy przy uruchomieniu, choć różnica bywa dziś mniejsza, niż sugerują starsze porównania, bo konkurencja też ograniczyła swój narzut.
Druga rzecz to podejście do formularzy, w którym ten framework wyróżnia się realnie. Akcje formularzy pozwalają obsłużyć wysłanie danych bez pisania osobnej trasy API i bez kodu po stronie przeglądarki. Formularz działa nawet wtedy, gdy JavaScript się nie wczyta, bo pod spodem jest zwykłe wysłanie formularza, a warstwa kliencka jedynie je ulepsza. To rozwiązanie, które w innych frameworkach trzeba świadomie budować, tutaj jest domyślne.
Trzecia to adaptery. Ten sam kod aplikacji wdrożysz na platformę bezserwerową, na własny serwer w Node.js albo jako stronę statyczną, zmieniając jeden wpis w konfiguracji. Nie jest to magia bez kosztów, bo część możliwości zależy od środowiska docelowego, ale przenosiny między dostawcami są tu wyraźnie tańsze niż w rozwiązaniach związanych z jedną platformą.
Funkcje zdalne, czyli najnowsza zmiana
To najświeższy element, o którym warto wiedzieć, bo zmienia sposób pisania warstwy danych i jest jednocześnie oznaczony jako eksperymentalny.
Funkcje zdalne pozwalają wywołać kod serwerowy bezpośrednio z komponentu, bez definiowania trasy API i bez ręcznego obsługiwania żądania. Piszesz funkcję, oznaczasz ją jako zapytanie i wywołujesz w komponencie, a framework zajmuje się przesłaniem żądania, typowaniem i pamięcią podręczną.
Warto znać dwa szczegóły, bo starsze poradniki podają nieaktualne informacje. Metoda uruchamiająca zapytanie została usunięta w wydaniu 2.61.0 i zastąpiona bezpośrednim oczekiwaniem na wynik we wszystkich kontekstach. Doszła też odmiana utrzymująca długotrwałą subskrypcję, przydatna przy danych zmieniających się w czasie.
Jest jednak zastrzeżenie, które trzeba postawić wprost. Mechanizm wymaga włączenia dwóch opcji eksperymentalnych naraz: obsługi asynchroniczności w kompilatorze i samych funkcji zdalnych w konfiguracji frameworka. Te dwie rzeczy są ze sobą sprzężone i próba użycia zapytań bez włączonej asynchroniczności nie zadziała. Przy projekcie produkcyjnym warto to potraktować jak każdą funkcję eksperymentalną: da się na niej budować, ale interfejs może się jeszcze zmienić.
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 a 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 dlx sv create my-app
# Z yarn
yarn dlx sv create my-app
# Z bunx
bunx sv create my-app
# Pakiet create-svelte, do ktorego prowadzily polecenia
# "pnpm create svelte" i "yarn create svelte", jest wycofany.
# npm zwraca przy nim komunikat odsylajacy do sv.Struktura 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 configTrasowanie oparte na plikach
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 |
Trasy dynamiczne
<!-- 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 }
}Parametry zbiorcze (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 }
}Parametry opcjonalne
// 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 }
}Grupy tras
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 # /settingsŁadowanie danych
Universal load (na serwerze i w kliencie)
// 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 (tylko na serwerze)
// 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>Dane z warstwy nadrzędnej
// 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 }
}Unieważnianie i przeładowanie
<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>Akcje formularzy
SvelteKit oferuje natywne formularze z progressive enhancement - działają bez JavaScript!
Podstawowe akcje formularzy
// 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>Akcje nazwane
// 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}Stopniowe wzbogacanie
<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>Punkty końcowe API
Podstawowe punkty końcowe
// 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 })
}Dynamiczne trasy API
// 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 })
}Odpowiedzi strumieniowane
// 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 wysyłanie
// 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 - stan reaktywny
<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 - wartości wyliczane
<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 - efekty uboczne
<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 - właściwości komponentu
<!-- 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 - wiązanie dwukierunkowe
<!-- 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>Hooki i warstwa pośrednia
Hooki serwerowe
// 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'
}
}Hooki klienckie
// 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'
}
}Wzorzec warstwy autoryzacji
// 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)
}Prerenderowanie i SSG
Konfiguracja prerenderowania
// 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)Prerenderowanie dynamiczne
// 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 }
}Wzorzec SSG i ISR
// 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)
}
}
}
}Adaptery wdrożeniowe
Adapter Auto (wykrywanie automatyczne)
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: 'nodejs22.x', // lub 'nodejs20.x', 'nodejs24.x', 'bun1.x'
regions: ['fra1'],
split: true, // Funkcje per-route
// runtime: 'edge' nadal dziala, ale jego wariant konfiguracji
// jest w adapterze oznaczony jako przestarzaly, a ISR go nie obsluguje
})
}
}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>
// $app/stores dziala, ale od SvelteKit 2.12 zastapil je $app/state
import { page } from '$app/state'
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'
// zod4 dla Zod 4, ktore instaluje sie domyslnie; adapter zod celuje w Zod 3
import { zod4 } from 'sveltekit-superforms/adapters'
import { z } from 'zod'
const schema = z.object({
email: z.email('Nieprawidłowy email'),
message: z.string().min(10, 'Minimum 10 znaków')
})
export const load = async () => {
const form = await superValidate(zod4(schema))
return { form }
}
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod4(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>Magazyny i zarządzanie stanem
Magazyny Svelte
// 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 do testów jednostkowych
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 do testów E2E
Dodatek konfiguruje Playwrighta, który uruchamia ten sam test w silnikach Chromium, Firefoksa i WebKita, a przed kliknięciem sam czeka na gotowość elementu zamiast polegać na sztucznych opóźnieniach. Same silniki pobiera osobne polecenie npx playwright install, więc pierwszy przebieg w potoku CI trwa dłużej niż kolejne.
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()
})
})Optymalizacja wydajności
Ładowanie leniwe
<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ć punkty końcowe API?
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, choć obraz się zmienił. Cloudflare uruchamia aplikację na brzegu sieci z definicji i nie ma tam osobnego przełącznika. W adapterze Vercela runtime: 'edge' nadal działa, ale wariant konfiguracji, do którego należy, jest oznaczony jako przestarzały, obsługiwane wartości uruchomieniowe to dziś nodejs20.x, nodejs22.x, nodejs24.x i bun1.x, a wycofane nodejs18.x kończy się błędem przy budowaniu. ISR działa wyłącznie w wariancie bezserwerowym, więc łączenie go z brzegiem nie wchodzi w grę.
Czy warto zaczynać nowy projekt na funkcjach zdalnych?
Zależy od tolerancji na zmiany. Mechanizm jest oznaczony jako eksperymentalny i wymaga włączenia dwóch opcji naraz, a jego interfejs zmieniał się jeszcze w bieżącym roku. Do prototypu i projektu wewnętrznego tak, do produktu, którego nie chcesz przepisywać za pół roku, bezpieczniejsze są funkcje ładujące dane i akcje formularzy.
Ładowanie danych, czyli miejsce najczęstszych pomyłek
Rozróżnienie między dwoma rodzajami funkcji ładujących dane jest tym, o co potyka się większość osób zaczynających z tym frameworkiem, a konsekwencje bywają poważne.
Funkcja uniwersalna wykonuje się i na serwerze, i w przeglądarce. Funkcja serwerowa wyłącznie na serwerze. Różnica w nazwie pliku jest jednoliterowa, a różnica w skutkach zasadnicza: kod umieszczony w funkcji uniwersalnej trafia do paczki wysyłanej użytkownikowi razem ze wszystkim, po co sięga.
Praktyczna reguła: wszystko, co dotyka bazy danych, kluczy albo zmiennych środowiskowych, musi być w wariancie serwerowym. Framework wprawdzie ostrzega przy próbie zaimportowania modułu serwerowego do kodu klienckiego, ale nie uchroni Cię przed wpisaniem klucza wprost w miejscu, w którym nie powinien się znaleźć.
Druga pułapka dotyczy tego, co taka funkcja zwraca. Wynik musi dać się zapisać i odtworzyć po drugiej stronie, więc obiekty z metodami, instancje klas i odwołania do elementów przeglądarki nie przejdą. Zwracaj proste struktury i buduj z nich to, czego potrzebujesz, dopiero w komponencie.
Trzecia to zależności między funkcjami. Wynik funkcji z wyższego poziomu trafia do niższych, więc dane wspólne dla całej sekcji serwisu warto pobrać raz na górze zamiast powtarzać w każdej podstronie. Odwrotny błąd też się zdarza: umieszczenie w układzie nadrzędnym zapytania potrzebnego na jednej podstronie sprawia, że wykonuje się ono przy każdym wejściu gdziekolwiek.
Czwarta to unieważnianie. Po akcji zmieniającej dane framework sam przeładuje odpowiednie funkcje, ale tylko te, które zadeklarowały zależność. Jeśli po zapisaniu formularza lista się nie odświeża, w dziewięciu przypadkach na dziesięć brakuje właśnie tej deklaracji.
Piąta, najbardziej podstępna, dotyczy strumieniowania. Funkcja ładująca może zwrócić obietnicę zamiast gotowej wartości, dzięki czemu strona wyświetli się natychmiast, a wolniejsze dane dopłyną później. Kuszące jest opakowanie w ten sposób wszystkiego, ale wtedy każda sekcja strony pojawia się osobno i interfejs skacze. Strumieniuj to, co faktycznie jest wolne i drugorzędne, na przykład listę rekomendacji obok treści głównej, a rzeczy niezbędne do zrozumienia strony pobieraj normalnie.
SvelteKit a alternatywy
| Cecha | SvelteKit | Next.js | Nuxt | Astro |
|---|---|---|---|---|
| Warstwa bazowa | Svelte, kompilator | React | Vue | dowolna, wyspy |
| Rozmiar kodu wysyłanego | najmniejszy | średni | średni | najmniejszy przy stronach treściowych |
| Obsługa formularzy bez API | wbudowana | akcje serwerowe | częściowo | ograniczona |
| Wymiana platformy wdrożenia | adapter, jedna linia | zależna od dostawcy | adapter | adapter |
| Wielkość ekosystemu | mała | największa | średnia | średnia |
| Dostępność osób na rynku | ograniczona | wysoka | średnia | ograniczona |
Wybór rozstrzyga się zwykle poza listą funkcji. Jeśli budujesz aplikację, w której formularze i praca z danymi stanowią trzon, a zespół jest mały i decyduje sam o technologii, ten framework daje najprzyjemniejszą pracę w tej stawce. Jeśli projekt ma żyć latami w większej organizacji i przechodzić przez ręce wielu osób, ekosystem i dostępność ludzi ważą więcej niż elegancja rozwiązania, a wtedy popularniejszy framework wygrywa mimo gorszych parametrów.
Dokumentację znajdziesz na stronie Svelte, a kod źródłowy w repozytorium projektu.