Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide43 min read

SvelteKit

SvelteKit is the official fullstack Svelte framework with SSR, file-based routing, form actions, and server endpoints. Compiles to native JavaScript with no runtime.

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

  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 vs 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 create svelte@latest my-app

# Z yarn
yarn create svelte my-app

# Z bunx
bunx sv create my-app

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

File-Based Routing

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

Dynamic Routes

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 }
}

Rest Parameters (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 }
}

Optional Parameters

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 }
}

Route Groups

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

Data Loading

Universal Load (runs on server & client)

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 (runs only on server)

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>

Parent Data

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 }
}

Invalidation i Reloading

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>

Form Actions

SvelteKit oferuje natywne formularze z progressive enhancement - działają bez JavaScript!

Podstawowe Form Actions

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>

Named Actions

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}

Progressive Enhancement

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>

API Endpoints

Podstawowe Endpoints

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 })
}

Dynamic API Routes

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 })
}

Streaming Responses

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 Upload

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 - Reactive State

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 - Computed Values

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 - Side Effects

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 - Component Props

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 - Two-way Binding

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>

Hooks i Middleware

Server Hooks

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'
  }
}

Client Hooks

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'
  }
}

Auth Middleware Pattern

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)
}

Prerendering i SSG

Konfiguracja Prerendering

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)

Dynamiczne Prerendering

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 }
}

SSG + ISR Pattern

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)
      }
    }
  }
}

Deployment Adapters

Adapter Auto (wykrywa automatycznie)

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: 'edge',  // lub 'nodejs18.x'
      regions: ['fra1'],
      split: true  // Funkcje per-route
    })
  }
}

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

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'
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
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>

Stores i State Management

Svelte Stores

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 dla Unit Tests

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 dla E2E

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()
  })
})

Performance Optimization

Lazy Loading

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ć 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

  1. No runtime - The compiler generates native JavaScript
  2. Fastest framework - Smaller bundles, faster hydration
  3. Simple mental model - Intuitive syntax with no boilerplate
  4. File-based routing - Folder structure = URL structure
  5. Form actions - Native forms with progressive enhancement
  6. Universal - SSR, SSG, SPA, all in one
  7. Svelte 5 Runes - Modern reactivity system
  8. Vite under the hood - Lightning-fast dev server

SvelteKit vs other frameworks

FeatureSvelteKitNext.jsNuxtRemix
RuntimeNoneReactVueReact
Bundle sizeSmallestMediumMediumSmall
Learning curveEasyMediumMediumMedium
Form handlingNativeClient-sideClient-sideNative
CompilerYesNoNoNo
Dev experienceExcellentGoodGoodGood
TypeScriptFullFullFullFull
Deploy adaptersManyVercel-firstManyMany

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

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

Alternative methods

Code
Bash
# With pnpm
pnpm create svelte@latest my-app

# With yarn
yarn create svelte my-app

# With bunx
bunx sv create my-app

Project structure

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

Configuring 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 = {
  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 config

File-based routing

Basic structure

SvelteKit uses the folder structure inside src/routes/ to define routing:

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

Special files

FileDescription
+page.sveltePage component
+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

Dynamic routes

src/routes/blog/[slug]/+page.svelte
SVELTE
<!-- 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>
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 not found'
    })
  }

  return { post }
}

Rest parameters (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"
  // for URL: /docs/getting-started/installation

  const segments = params.path.split('/')
  const doc = await fetchDoc(segments)

  return { doc, breadcrumbs: segments }
}

Optional parameters

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

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

Data loading

Universal load (runs on server and client)

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

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 }) => {
  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

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 />
  <footer>Footer</footer>
</div>

Parent data

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

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

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: 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
SVELTE
<!-- 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

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="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

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') {
        await update()
      }
    }
  }}
>
  <input name="title" disabled={loading} />
  <button disabled={loading}>
    {loading ? 'Adding...' : 'Add'}
  </button>
</form>

API endpoints

Basic endpoints

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()

  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

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, '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

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'
    }
  })
}

Files and upload

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, '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

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

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

Code
SVELTE
<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
SVELTE
<!-- 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
SVELTE
<!-- 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
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>Typed text: {text}</p>

Hooks and middleware

Server hooks

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

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)

  await trackError(error)

  return {
    message: 'Something went wrong'
  }
}

Auth middleware pattern

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 }) => {
  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

TSsrc/routes/blog/+page.ts
TypeScript
// src/routes/blog/+page.ts
export const prerender = true

// src/routes/admin/+page.ts
export const prerender = false

Dynamic prerendering

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

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

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: 'edge',
      regions: ['fra1'],
      split: true
    })
  }
}

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>']
      }
    })
  }
}

Integrations

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 for 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>
  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

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'
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
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>
    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

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()

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>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
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
<!-- 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

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 for E2E

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=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

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()
  })
})

Performance optimization

Lazy loading

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>Loading...</p>
{/if}

Streaming

TSsrc/routes/dashboard/+page.server.ts
TypeScript
// src/routes/dashboard/+page.server.ts
export const load = async () => {
  return {
    // Immediate
    user: await getUser(),

    // Streamed (does not block rendering)
    stats: getStats(),
    notifications: getNotifications()
  }
}
Code
SVELTE
<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

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 - 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.