Beim ersten Öffnen eines neuen SvelteKit-Projekts siehst du etwa fünfzehn Dateien und Ordner — und fragst dich erst mal, was wofür ist. Die gute Nachricht: Nur eine Handvoll davon brauchst du im Alltag wirklich. Der Rest ist Konfiguration, die einmal eingerichtet ist und in Ruhe gelassen wird. Dieser Artikel geht das Projekt von außen nach innen durch und erklärt jede Datei und jeden Ordner so, dass du weißt, was du anfassen darfst und was es bedeutet, wenn du etwas verschiebst.
Die Struktur auf einen Blick
Nach npx sv create my-app mit dem Skeleton-Template sieht das Projekt etwa so aus:
my-app/
├── src/
│ ├── lib/
│ │ └── index.ts
│ ├── routes/
│ │ ├── +layout.svelte
│ │ └── +page.svelte
│ ├── app.html
│ ├── app.d.ts
│ ├── app.css
│ └── hooks.server.ts
├── static/
│ └── favicon.svg
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
└── package.jsonDrei Hauptbereiche:
src/— der Quellcode, den du täglich anfasst.static/— Dateien, die unverändert ans Hosting kommen.- Wurzel-Konfiguration — wie SvelteKit, Vite und TypeScript konfiguriert sind.
src/routes/ — die Seiten der App
Hier verbringst du als Entwickler die meiste Zeit. Jeder Ordner ist ein Stück URL. Eine Datei mit dem Namen +page.svelte macht aus diesem Ordner eine echte Seite, die im Browser sichtbar ist.
Klingt abstrakt? Dann ein Beispiel: Wenn du den Ordner routes/about/ anlegst und dort eine +page.svelte reinlegst, dann ist die URL /about der App automatisch eine Seite. Du musst nichts in eine Konfig-Datei eintragen, kein Routing-Setup machen — der Ordnername ist der Pfad.
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/<slug>
└── api/
└── ping/
└── +server.ts → /api/ping (kein Markup, nur HTTP-Handler)Die Datei-Konvention +page.svelte, +layout.svelte, +server.ts ist hart kodiert — der +-Präfix sagt SvelteKit „das ist eine Spezial-Datei”. Eigene Dateien ohne +-Präfix (z. B. eine helpers.ts daneben) werden ignoriert beim Routing, sind aber als normale Module nutzbar.
Tieferes zur Datei-Konvention im Artikel +page, +layout, +server, +error.
src/lib/ — wiederverwendbarer Code
Wenn du etwas an mehreren Stellen brauchst — eine Button-Komponente, eine Datums-Formatierungs-Funktion, eine Datenbank-Verbindung — dann gehört es in src/lib/. Damit du es nicht mit relativen Pfaden importieren musst (../../../lib/components/Button.svelte), gibt es eine Abkürzung: $lib. Damit funktioniert der Import von überall im Projekt:
src/lib/
├── components/
│ ├── Button.svelte
│ ├── Card.svelte
│ └── Modal.svelte
├── server/
│ ├── db.ts
│ └── auth.ts
├── stores/
│ └── theme.svelte.ts
├── utils/
│ └── date.ts
└── index.tsImporte aus $lib/... funktionieren überall im Projekt, unabhängig davon, wie tief die nutzende Datei verschachtelt ist:
import Button from '$lib/components/Button.svelte';
import { formatDate } from '$lib/utils/date';Innerhalb von lib/ gibt es einen besonders wichtigen Unter-Ordner: src/lib/server/. Was dort liegt, läuft nur auf dem Server — nie im Browser des Nutzers.
Warum ist das wichtig? Stell dir vor, du legst die Verbindung zur Datenbank in lib/db.ts ab, mit dem Datenbank-Passwort darin. Wenn du diese Datei aus einer Komponente importierst, landet das Passwort versehentlich im Browser-Bundle — jeder Nutzer kann es lesen. Genau das verhindert SvelteKit für lib/server/: Versuchst du, eine Datei daraus aus Client-Code zu importieren, gibt es einen Build-Fehler. Du wirst gestoppt, bevor das Geheimnis nach außen kommt.
Faustregel: Was Datenbank-Verbindungen, API-Keys oder serverseitige Hilfsfunktionen sind, gehört nach lib/server/. Komponenten, die sowohl Server als auch Client zeigen sollen, gehören nach lib/components/ oder direkt in lib/.
src/app.html — das HTML-Skelett
Die Datei app.html ist die Vorlage, in die SvelteKit den gerenderten Inhalt einbettet:
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>Die zwei Platzhalter sind wichtig:
%sveltekit.head%— hier landen alle<svelte:head>-Inhalte (Title, Meta-Tags) und Asset-Links, die SvelteKit injiziert.%sveltekit.body%— hier landet der gerenderte App-Inhalt.
Du kannst die Datei anpassen, um zum Beispiel ein eigenes Favicon-Set einzubauen, Analytics-Tags zu ergänzen oder einen <noscript>-Block für JS-deaktivierte Browser zu zeigen. Was du aber nicht entfernen darfst: die zwei Platzhalter und das <div> um %sveltekit.body%.
src/app.d.ts — globale Types
Hier deklarierst du Types, die SvelteKit-spezifisch sind und in der ganzen App verfügbar sein sollen. Der Standard-Inhalt:
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};Vier Interfaces sind hier zentral:
App.Locals— Daten, die dein Server-Hook (hooks.server.ts) anhängt und die in jeder Server-Route verfügbar sind. Klassisch:useraus dem Cookie,db-Verbindung.App.PageData— Form, die jedeload-Funktion mindestens zurückgibt. Praktisch für globale Werte (z. B. immerseo: SeoData).App.Error— Form von Fehlern, dieerror(...)produziert. Default ist{ message: string }.App.Platform— Adapter-spezifische Plattform-Informationen (z. B. Cloudflare-Bindings).
Wer Auth-Logik schreibt, definiert hier typisch Locals:
declare global {
namespace App {
interface Locals {
user: { id: string; email: string } | null;
}
}
}
export {};Ab dann ist event.locals.user in jeder Server-Route typisiert verfügbar.
src/hooks.server.ts — Server-Middleware
Der handle-Hook in hooks.server.ts läuft bei jedem Server-Request. Hier hängst du Auth-Prüfung, Logging, Header-Manipulation und so weiter ein.
import type { Handle } from '@sveltejs/kit';
import { getUserFromCookie } from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('session');
event.locals.user = sessionId ? await getUserFromCookie(sessionId) : null;
return resolve(event);
};Was hier passiert:
- Vor jedem Request greifen wir auf den Session-Cookie zu.
- Wenn vorhanden, holen wir den User aus der Datenbank und legen ihn an
event.locals.userab. resolve(event)setzt die Verarbeitung fort — SvelteKit rendert die eigentliche Route.
Im Server-Code jeder Route ist event.locals.user jetzt verfügbar — ohne das Cookie selbst noch mal anzufassen.
Es gibt auch hooks.client.ts (für Client-seitige Lifecycle-Hooks) und hooks.ts (universelle Hooks). Server-Hook ist aber der mit Abstand häufigste Use Case.
static/ — unbearbeitete Dateien
Alles in static/ wird unverändert als statische Datei ausgeliefert, an der gleichen URL-Position wie im Verzeichnis.
static/
├── favicon.svg → /favicon.svg
├── robots.txt → /robots.txt
├── images/
│ └── logo.png → /images/logo.png
└── fonts/
└── inter.woff2 → /fonts/inter.woff2Wann gehört etwas hier hin?
- Favicons und ähnliche Asset-Standards mit fixer URL.
robots.txt,sitemap.xml-Dateien,.well-known/...-Pfade.- Bilder, die nicht im Komponenten-Code referenziert werden (z. B. Open-Graph-Bilder, die per Meta-Tag eingebunden werden).
- Schriftarten — wobei viele Setups sie auch direkt aus
node_modulesimportieren.
Was nicht in static/ gehört: Bilder oder Assets, die dein Komponenten-Code importiert. Importierte Assets gehen durch Vite und werden gehasht (Cache-Busting) und optimiert. static/ umgeht das.
Konfigurations-Dateien in der Wurzel
svelte.config.js — die Hauptkonfiguration für SvelteKit. Hier wählst du den Adapter, definierst Path-Aliase und kannst Preprocessor (Tailwind, MDX) einhängen:
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};vite.config.ts — die Vite-Konfiguration. Hier kommen Vite-Plugins rein (Tailwind, eigene Plugins) und Build-Optimierungen.
tsconfig.json — die TypeScript-Konfiguration. Verweist typisch auf .svelte-kit/tsconfig.json, wo SvelteKit eigene Path-Aliase ($lib, $app/*) und Type-Definitionen einträgt. Die generierte Datei landet in .svelte-kit/ (nicht commiten).
package.json — wie üblich, mit den Scripts dev, build, preview, check (siehe Setup-Artikel).
.svelte-kit/ — generierte Dateien (nicht commiten)
Beim ersten npm run dev oder npm run build legt SvelteKit ein Verzeichnis .svelte-kit/ an. Darin: generierte Type-Definitionen, virtuelle Module, Routing-Cache. Diese Dateien werden bei jedem Build neu erzeugt — sie gehören nicht ins Git-Repository.
Im .gitignore ist der Eintrag standardmäßig schon dabei. Falls nicht: hinzufügen.
Besonderheiten der Projektstruktur
Ein paar Eigenheiten, die nicht selbsterklärend sind und die man einmal gehört haben sollte:
Der +-Präfix ist hart kodiert.
Nur Dateien, die mit + beginnen (+page.svelte, +layout.svelte, +server.ts, +error.svelte), werden vom Routing erkannt. Das hat einen praktischen Vorteil: Du kannst beliebig andere Dateien daneben legen — Button.svelte, helpers.ts, eine README.md — und sie tauchen nicht als Routen auf. Sie sind einfach „normaler” Code in der Nähe der Route.
lib/server/ ist ein vom Compiler verfolgtes Geheimnis-Versteck.
Eine Datei dort kann technisch nicht in den Client-Bundle wandern. Das ist kein Konvention, sondern wird von SvelteKit hart erzwungen. Damit kannst du z. B. ein API-Token oder eine Datenbank-Verbindung ablegen, ohne dass dir versehentlich ein import { db } from '$lib/server/db' aus einer Komponente alles zerlegt.
Der $lib-Alias funktioniert auch außerhalb von src/lib/.
Du kannst den Alias überall in deinem Projekt nutzen — auch in vite.config.ts für Test-Setups, oder in eigenen Vite-Plugins. Der Alias zeigt einfach immer auf das src/lib/-Verzeichnis.
static/ umgeht den Build komplett.
Während Bilder, die du im Code importierst (import logo from '$lib/logo.svg'), gehasht und optimiert werden, gehen Dateien aus static/ unverändert ans Hosting. Das ist nützlich für Dateien mit fixer URL (favicon.svg, robots.txt) — und ein potenzielles Problem für Bilder, die du eigentlich optimieren wolltest.
.svelte-kit/ ist nur Generierter Code.
Beim ersten npm run dev taucht ein Verzeichnis .svelte-kit/ auf. Es enthält generierte Types und virtuelle Module, die SvelteKit zur Build-Zeit erzeugt. Du fasst es nie an, du committest es nie, du änderst nie etwas darin. Wenn TypeScript komische Fehler zeigt, hilft oft npx svelte-kit sync — das regeneriert den Inhalt.
app.html ist die einzige reguläre HTML-Datei.
Alles andere in der App sind .svelte-Komponenten. Nur die app.html ist wirklich HTML — und auch sie ist nur ein Template, in das SvelteKit den Inhalt einfügt. Die zwei Platzhalter %sveltekit.head% und %sveltekit.body% müssen drinbleiben, sonst kommt beim Build ein Fehler.
Konfigurations-Dateien sind ESM, nicht CommonJS.
svelte.config.js und vite.config.ts werden als ECMAScript-Module geladen. Wer aus älteren Setups module.exports = ... gewohnt ist, schreibt jetzt export default .... Das ist keine SvelteKit-Eigenheit, sondern Vite-Standard.