Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik27 min czytania

SvelteKit, fullstackowy framework aplikacji Svelte

SvelteKit to oficjalny framework do aplikacji w Svelte. Routing z plików, akcje formularzy, funkcje zdalne, adaptery wdrożeniowe i typowe błędy.

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

  1. Brak runtime - Kompilator generuje natywny JavaScript
  2. Najszybszy framework - Mniejsze paczki, szybsze hydration
  3. Prosty mental model - Intuicyjna składnia bez boilerplate
  4. File-based routing - Struktura folderów = struktura URL
  5. Form actions - Natywne formularze z progressive enhancement
  6. Uniwersalny - SSR, SSG, SPA, wszystko w jednym
  7. Svelte 5 Runes - Nowoczesny system reaktywności
  8. Vite pod spodem - Błyskawiczny dev server

SvelteKit a inne frameworki

CechaSvelteKitNext.jsNuxtRemix
RuntimeBrakReactVueReact
Rozmiar bundleNajmniejszyŚredniŚredniMały
Learning curveŁatwaŚredniaŚredniaŚrednia
Form handlingNatywneClient-sideClient-sideNatywne
KompilatorTakNieNieNie
Dev experienceŚwietnyDobryDobryDobry
TypeScriptPełnePełnePełnePełne
Adaptery deployWieleVercel-firstWieleWiele

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

Code
Bash
# 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 dev

Alternatywne metody

Code
Bash
# 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

Code
TEXT
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.json

Konfiguracja svelte.config.js

JSsvelte.config.js
JavaScript
// 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 config

Trasowanie oparte na plikach

Podstawowa struktura

SvelteKit używa struktury folderów w src/routes/ do definiowania routingu:

Code
TEXT
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      # /register

Pliki specjalne

PlikOpis
+page.svelteKomponent strony
+page.tsUniversal load function
+page.server.tsServer-only load function
+layout.svelteLayout wrapper
+layout.tsLayout load function
+layout.server.tsServer-only layout load
+server.tsAPI endpoint
+error.svelteError boundary

Trasy dynamiczne

src/routes/blog/[slug]/+page.svelte
SVELTE
<!-- 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>
TSsrc/routes/blog/[slug]/+page.server.ts
TypeScript
// 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)

TSsrc/routes/docs/[...path]/+page.server.ts
TypeScript
// 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

TSsrc/routes/[[lang]]/about/+page.server.ts
TypeScript
// 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:

Code
TEXT
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)

TSsrc/routes/blog/+page.ts
TypeScript
// 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)

TSsrc/routes/blog/+page.server.ts
TypeScript
// 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

TSsrc/routes/+layout.server.ts
TypeScript
// 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
SVELTE
<!-- 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

TSsrc/routes/dashboard/+page.ts
TypeScript
// 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

Code
SVELTE
<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

TSsrc/routes/contact/+page.server.ts
TypeScript
// 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
SVELTE
<!-- 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

TSsrc/routes/posts/+page.server.ts
TypeScript
// 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
SVELTE
<!-- 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

Code
SVELTE
<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

TSsrc/routes/api/users/+server.ts
TypeScript
// 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

TSsrc/routes/api/users/[id]/+server.ts
TypeScript
// 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

TSsrc/routes/api/stream/+server.ts
TypeScript
// 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

TSsrc/routes/api/upload/+server.ts
TypeScript
// 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

Code
SVELTE
<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

Code
SVELTE
<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

Code
SVELTE
<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
SVELTE
<!-- 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
SVELTE
<!-- 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
SVELTE
<!-- Input.svelte -->
<script>
  let { value = $bindable() } = $props()
</script>

<input bind:value />
Parent.svelte
SVELTE
<!-- 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

TSsrc/hooks.server.ts
TypeScript
// 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

TSsrc/hooks.client.ts
TypeScript
// 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

TSsrc/hooks.server.ts
TypeScript
// 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

TSsrc/routes/blog/+page.ts
TypeScript
// 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

TSsrc/routes/blog/[slug]/+page.server.ts
TypeScript
// 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

JSsvelte.config.js
JavaScript
// 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)

Code
Bash
npm install @sveltejs/adapter-auto
JSsvelte.config.js
JavaScript
// svelte.config.js
import adapter from '@sveltejs/adapter-auto'

const config = {
  kit: {
    adapter: adapter()
  }
}

Adapter Node.js

Code
Bash
npm install @sveltejs/adapter-node
Code
JavaScript
import adapter from '@sveltejs/adapter-node'

const config = {
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true,
      envPrefix: 'MY_APP_'
    })
  }
}

Adapter Static (SSG)

Code
Bash
npm install @sveltejs/adapter-static
Code
JavaScript
import adapter from '@sveltejs/adapter-static'

const config = {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: '404.html',
      precompress: false
    })
  }
}

Adapter Vercel

Code
Bash
npm install @sveltejs/adapter-vercel
Code
JavaScript
import 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

Code
Bash
npm install @sveltejs/adapter-cloudflare
Code
JavaScript
import adapter from '@sveltejs/adapter-cloudflare'

const config = {
  kit: {
    adapter: adapter({
      routes: {
        include: ['/*'],
        exclude: ['<all>']
      }
    })
  }
}

Integracje

Tailwind CSS

Code
Bash
npx sv add tailwindcss
+page.svelte
SVELTE
<!-- +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

Code
Bash
npm install prisma @prisma/client
npx prisma init
TSsrc/lib/server/database.ts
TypeScript
// 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)

Code
Bash
npm install @auth/sveltekit @auth/core
TSsrc/hooks.server.ts
TypeScript
// 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
SVELTE
<!-- 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

Code
Bash
npm install sveltekit-superforms zod
TSsrc/routes/contact/+page.server.ts
TypeScript
// 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
SVELTE
<!-- 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

TSsrc/lib/stores/cart.ts
TypeScript
// 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)
)
Code
SVELTE
<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
SVELTE
<!-- 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 />
Code
SVELTE
<!-- 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

Code
Bash
npx sv add vitest
TSsrc/lib/utils.test.ts
TypeScript
// 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.

Code
Bash
npx sv add playwright
TStests/home.test.ts
TypeScript
// 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

Code
Bash
npm install @testing-library/svelte
TSsrc/lib/components/Counter.test.ts
TypeScript
// 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

Code
SVELTE
<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

TSsrc/routes/dashboard/+page.server.ts
TypeScript
// 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()
  }
}
Code
SVELTE
<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

TSsrc/service-worker.ts
TypeScript
// 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

CechaSvelteKitNext.jsNuxtAstro
Warstwa bazowaSvelte, kompilatorReactVuedowolna, wyspy
Rozmiar kodu wysyłanegonajmniejszyśredniśredninajmniejszy przy stronach treściowych
Obsługa formularzy bez APIwbudowanaakcje serweroweczęściowoograniczona
Wymiana platformy wdrożeniaadapter, jedna liniazależna od dostawcyadapteradapter
Wielkość ekosystemumałanajwiększaśredniaśrednia
Dostępność osób na rynkuograniczonawysokaśredniaograniczona

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.