Im routes/-Verzeichnis arbeitet SvelteKit mit reservierten Dateinamen. Eine Datei, die mit einem + beginnt, hat eine besondere Bedeutung — +page.svelte ist eine Seite, +layout.svelte ist eine Hülle drumherum, +server.ts ist ein API-Endpunkt, +error.svelte ist eine Fehlerseite. Dazu kommen .server.ts-Varianten für Code, der nur auf dem Server laufen darf. Klingt erst mal nach viel — am Ende sind es aber nur eine Handvoll Bausteine, mit denen du jede mögliche Route in SvelteKit baust. Dieser Artikel geht alle durch, mit Beispielen für jeden Anwendungsfall.
+page.svelte — die Seite selbst
Das ist die häufigste Spezial-Datei und enthält das, was der Nutzer auf der Seite sieht: HTML, Komponenten, Interaktionen.
<script>
let { data } = $props();
</script>
<h1>Über uns</h1>
<p>{data.text}</p>Drei Dinge zu beachten:
- Die
data-Prop kommt automatisch — SvelteKit übergibt darin den Rückgabewert der zugehörigenload-Funktion. - Wenn keine
load-Funktion existiert, istdataein leeres Objekt. - Die Komponente kann
<svelte:head>für SEO-Tags nutzen, alle normalen Komponenten-Features sind verfügbar.
+page.ts und +page.server.ts — Daten für die Seite
Eine reine +page.svelte zeigt nur statisches Markup. Sobald deine Seite Daten braucht — User-Liste aus der Datenbank, Blog-Artikel aus einem CMS, Wetterdaten von einer API — lädst du sie in einer separaten Datei: einer load-Funktion in +page.ts oder +page.server.ts. Die Datei liegt direkt neben +page.svelte im gleichen Ordner.
Den Unterschied zwischen den beiden musst du verstehen:
+page.ts läuft universal — also sowohl auf dem Server (beim ersten Aufruf) als auch im Browser (wenn der Nutzer durch die App navigiert). Geeignet für Daten, die von einer öffentlichen API kommen, bei der es egal ist, wer den Request stellt.
export async function load({ fetch }) {
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
return { posts };
}+page.server.ts läuft nur auf dem Server. Das ist die richtige Wahl, sobald du auf eine Datenbank zugreifst, ein API-Token verwendest oder mit Cookies arbeitest. Der Code dieser Datei landet nie im Browser-Bundle, du kannst also bedenkenlos Geheimnisse darin haben.
import { db } from '$lib/server/db';
export async function load({ locals }) {
if (!locals.user) return { user: null };
const stats = await db.stats.findMany({ where: { userId: locals.user.id } });
return { user: locals.user, stats };
}Du kannst beide Varianten kombinieren — +page.server.ts läuft zuerst, dann +page.ts (mit den Server-Daten als Eingabe). Das ist ein nützliches Pattern, wenn ein Teil der Daten Server-only ist und ein anderer Teil clientseitig ergänzt wird.
Mehr zur load-Funktion im Artikel SvelteKit Routing & Loading.
+layout.svelte — gemeinsame Hülle
Eine +page.svelte ist eine einzelne Seite. Aber praktisch jede App hat wiederkehrende Bestandteile — Header, Footer, Sidebar, Navigation. Du willst diese nicht in jeder einzelnen Page-Datei wiederholen.
Genau das ist die Aufgabe von +layout.svelte. Die Datei umschließt alle Routen, die unterhalb ihres Ordners liegen. Das zentrale Layout in src/routes/+layout.svelte umschließt die ganze App — Header, Footer, App-weite Stylesheets. Innerhalb davon kannst du beliebig viele Sub-Layouts haben, etwa eines für den Dashboard-Bereich, eines für den Blog.
<script>
let { children } = $props();
</script>
<header>
<a href="/">Home</a>
<a href="/about">Über</a>
</header>
<main>
{@render children()}
</main>
<footer>© 2026 mibeon</footer>Der children-Prop ist ein Snippet, in dem die eigentliche Seite gerendert wird. Du kannst rundherum so viel Markup haben, wie du willst.
Verschachtelte Layouts
Layouts stapeln sich automatisch. Wenn du in routes/dashboard/ eine zusätzliche +layout.svelte anlegst, wird sie innerhalb des App-Layouts gerendert. Praktisches Bild: Die App-Hülle ist der Karton, das Dashboard-Layout ist eine kleinere Box darin, die Page ist der Inhalt der Box.
src/routes/
├── +layout.svelte App-Hülle (immer da)
├── +page.svelte /
└── dashboard/
├── +layout.svelte Dashboard-Sidebar
├── +page.svelte /dashboard
└── settings/+page.svelte /dashboard/settings/dashboard/settings rendert in dieser Reihenfolge: App-Layout → Dashboard-Layout → Settings-Page. Jedes Layout zeigt seine Inhalte plus den Render-Slot des nächsten Levels.
Layouts mit Daten
Wie Pages können auch Layouts +layout.ts oder +layout.server.ts haben — für Daten, die alle untergeordneten Seiten brauchen (z. B. den eingeloggten User, eine Liste der Sidebar-Items).
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) {
if (!locals.user) redirect(303, '/login');
return { user: locals.user };
}Mit redirect wird nicht-authenticated traffic auf /login umgeleitet — alle Sub-Routen profitieren davon, ohne ihre eigene Auth-Prüfung zu machen.
+server.ts — API-Endpunkte ohne Markup
Bisher haben wir Seiten gebaut — etwas, das der Nutzer im Browser sieht. Manchmal brauchst du aber das Gegenteil: einen reinen API-Endpunkt, der JSON zurückgibt, ohne irgendein Markup. Klassische Anwendung: Eine Mobile-App fragt deine API ab, ein anderer Server schickt Webhook-Events, ein externes System holt sich Daten.
Dafür gibt es +server.ts. Eine Datei dieses Namens ohne +page.svelte-Geschwister macht aus dem Ordner einen API-Endpunkt — ohne Browser-Anzeige, nur HTTP-Method-Handler.
import { json } from '@sveltejs/kit';
export function GET() {
return json({ status: 'ok', timestamp: Date.now() });
}Die URL /api/health antwortet jetzt mit JSON. Alle HTTP-Methoden sind möglich:
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
export async function GET() {
const todos = await db.todo.findMany();
return json(todos);
}
export async function POST({ request }) {
const data = await request.json();
if (!data.text) error(400, 'text fehlt');
const todo = await db.todo.create({ data });
return json(todo, { status: 201 });
}Wann nutzt du +server.ts?
- Externe API-Konsumenten: Mobile App, Drittsystem, Webhook-Empfänger.
- Asynchrone Aktionen ohne Form (z. B. via fetch in einem Handler).
- Statische Dateien dynamisch generieren (sitemap.xml, RSS-Feed).
Wann nicht? Wenn du nur ein Form-Submit verarbeiten willst — dafür sind Form Actions (+page.server.ts mit actions) das passende Werkzeug. Form Actions können an die gleiche URL submitten wie die Seite selbst, ohne separate API-Route.
+error.svelte — Fehlerseite pro Bereich
Was passiert, wenn beim Laden einer Seite etwas schiefgeht? Etwa weil der Datenbank-Eintrag nicht existiert, oder der Nutzer nicht eingeloggt ist? Standardmäßig zeigt SvelteKit eine eingebaute Fehlerseite mit der Status-Nummer und einer kurzen Meldung. Funktional, aber nicht hübsch.
Mit +error.svelte baust du eine eigene Fehlerseite. Das Praktische: Du kannst sie pro Bereich unterschiedlich machen. Eine App-weite Fehlerseite in routes/+error.svelte, eine spezifischere in routes/shop/+error.svelte für Shop-Fehler. SvelteKit nutzt automatisch die spezifischste, die sich auf dem Pfad findet.
<script>
import { page } from '$app/state';
</script>
<h1>{page.status}</h1>
<p>{page.error?.message ?? 'Ein Fehler ist aufgetreten.'}</p>
<a href="/">Zur Startseite</a>page.status ist der HTTP-Statuscode (404, 500, …). page.error enthält das Error-Objekt — typisch mit message, eigene Felder kannst du in app.d.ts deklarieren.
Error-Boundaries pro Bereich
Wie Layouts sind auch +error.svelte-Dateien verschachtelt: SvelteKit nutzt die spezifischste, die im aktuellen Pfad auftaucht.
src/routes/
├── +error.svelte Fallback für die ganze App
└── shop/
├── +error.svelte eigene Shop-Fehlerseite
└── products/+page.svelteEin Fehler in /shop/products zeigt die Shop-spezifische Fehlerseite, ein Fehler in /about die App-weite. So kannst du je nach Bereich Tonalität, Markenfarben oder Hilfslinks anpassen.
Übersicht aller Spezial-Dateien
| Datei | Wann/Wofür |
|---|---|
+page.svelte | Seite (Markup) |
+page.ts | Universal-load (läuft Client und Server) |
+page.server.ts | Server-only-load und Form Actions |
+layout.svelte | Layout-Hülle für sich selbst und alle Unter-Routen |
+layout.ts | Universal-load für Layout |
+layout.server.ts | Server-only-load für Layout |
+server.ts | API-Endpunkt mit GET/POST/PATCH/PUT/DELETE/OPTIONS |
+error.svelte | Fehlerseite |
Diese Dateien sind reservierte Namen — andere Dateien im gleichen Ordner werden nicht als Routen behandelt. Du kannst eine helpers.ts neben +page.svelte legen und wie ein normales Modul importieren.
Beispiel: Eine vollständige Route mit allen Spezial-Dateien
Ein Blog-Eintrag mit Layout, Daten-Loading, Fehlerbehandlung und Form-Action für Kommentare:
src/routes/blog/[slug]/
├── +layout.svelte umgebendes Layout (Sidebar mit Inhalt)
├── +page.svelte eigentliche Anzeige
├── +page.server.ts load + Form Action für Kommentare
└── +error.svelte eigene 404 für nicht-gefundene Postsimport { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
export async function load({ params }) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) error(404, 'Post nicht gefunden');
return { post };
}
export const actions = {
comment: async ({ request, params, locals }) => {
if (!locals.user) error(401, 'Bitte einloggen');
const data = await request.formData();
await db.comment.create({
data: {
text: data.get('text') as string,
postSlug: params.slug,
userId: locals.user.id,
},
});
return { success: true };
},
};<script>
import { enhance } from '$app/forms';
let { data, form } = $props();
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.body}</div>
</article>
<form method="POST" action="?/comment" use:enhance>
<textarea name="text" required></textarea>
<button>Kommentieren</button>
</form>
{#if form?.success}
<p>Danke für deinen Kommentar.</p>
{/if}Eine handvoll Dateien, eine vollständige Funktion: Datenbank-Lookup, Fehler-Behandlung, Markup, Form-Submit. Genau das ist die Stärke der SvelteKit-Konvention.
Häufige Stolperfallen
+page.ts mit Code, der nicht im Browser laufen darf.
+page.ts läuft auch im Browser. Wer dort Datenbank-Code oder Secrets nutzt, leakt sie. Solche Logik gehört in +page.server.ts.
+layout für Auth-Schutz vergessen.
Eine redirect() in einem Layout-load schützt alle untergeordneten Routen. Wer den Schutz pro Page einzeln macht, vergisst irgendwann eine.
Server-Endpoint statt Form Action.
Wer ein Form-Submit verarbeitet, baut oft fälschlicherweise einen +server.ts-Endpoint plus Client-Fetch. Form Actions sind dafür einfacher — eine Datei, kein eigener Endpoint, automatisches Progressive Enhancement.
+error.svelte rendert ohne Daten.
Im Error-Render läuft die load-Funktion nicht (sie hat ja gerade gefailt). Du hast Zugriff auf page.error und page.status, aber kein data.
Datei-Namen mit Tippfehler.
+page.svelt (ohne e) oder +layout.tsx werden ignoriert — keine Fehlermeldung, einfach kein Routing. Wenn eine Route nicht erscheint, ist das oft die Ursache.
Weiterführende Ressourcen
Externe Quellen
- Routing – svelte.dev
+page– svelte.dev+layout– svelte.dev+server– svelte.dev+error– svelte.dev