Bisher haben wir Seiten gebaut — etwas, das der Nutzer im Browser sieht. Aber manchmal brauchst du das Gegenteil: einen Endpunkt, der JSON statt HTML zurückgibt. Eine Mobile-App fragt deine API ab, ein externer Webhook liefert Daten, ein Hintergrund-Job prüft den Status. Genau dafür gibt es in SvelteKit +server.ts. Eine Datei dieses Namens macht aus einem Ordner einen reinen API-Endpunkt — ohne Markup, ohne Page, nur HTTP-Handler.

Der einfachste API-Endpunkt

ts src/routes/api/zeit/+server.ts
import { json } from '@sveltejs/kit';

export function GET() {
    return json({
        jetzt: new Date().toISOString(),
        zone: 'Europe/Berlin',
    });
}

Was hier passiert:

  1. Die Datei liegt in routes/api/zeit/. Das macht die URL /api/zeit.
  2. Der Export GET wird automatisch zum Handler für GET-Requests auf diese URL.
  3. Die json(...)-Funktion baut eine Antwort mit dem richtigen Content-Type (application/json).
  4. Das Objekt landet als JSON-Body in der Antwort.

Aufruf vom Terminal:

bash curl-Aufruf
curl http://localhost:5173/api/zeit
# → { "jetzt": "2026-05-03T13:42:00.000Z", "zone": "Europe/Berlin" }

Alle HTTP-Methoden

+server.ts unterstützt alle gängigen HTTP-Methoden. Du exportierst pro Methode eine Funktion:

ts src/routes/api/produkte/+server.ts
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';

export async function GET() {
    const produkte = await db.produkt.findMany();
    return json(produkte);
}

export async function POST({ request }) {
    const data = await request.json();
    if (!data.name) error(400, 'name fehlt');

    const produkt = await db.produkt.create({ data });
    return json(produkt, { status: 201 });
}

Folgende Methoden sind möglich: GET, POST, PUT, PATCH, DELETE, OPTIONS und HEAD. Es gibt zusätzlich einen fallback-Export, der für alle Methoden greift, die nicht eigens exportiert wurden.

Dynamische Parameter

API Routes nutzen die gleichen Routing-Konventionen wie Pages — eckige Klammern für dynamische Segmente, [...rest] für Catch-all.

ts src/routes/api/produkte/[id]/+server.ts
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';

export async function GET({ params }) {
    const produkt = await db.produkt.findUnique({
        where: { id: params.id },
    });
    if (!produkt) error(404, 'Produkt nicht gefunden');
    return json(produkt);
}

export async function DELETE({ params }) {
    await db.produkt.delete({ where: { id: params.id } });
    return new Response(null, { status: 204 });
}

URL /api/produkte/42 setzt params.id = '42'. Bei DELETE antworten wir mit 204 No Content — eine Antwort ohne Body, aber mit Status-Code.

Was steckt im event-Argument?

Genau wie load-Funktionen bekommen +server.ts-Handler ein event-Argument. Die wichtigsten Felder:

FeldBeschreibung
requestDas ankommende Request-Objekt (Body, Header, Methode)
paramsDynamische Pfad-Parameter
urlDie URL als URL-Objekt (mit searchParams)
cookiesCookie-Lese- und Schreibzugriff
localsEigene Server-Daten aus dem handle-Hook (z. B. der User)
getClientAddressIP des Client (vom Adapter abhängig)
setHeadersEigene Response-Header setzen

Typische Verwendung — Auth-Prüfung plus Body-Parsing:

ts Auth + Body
import { json, error } from '@sveltejs/kit';

export async function POST({ request, locals }) {
    if (!locals.user) error(401, 'Nicht eingeloggt');

    const body = await request.json();
    // ...verarbeiten

    return json({ ok: true });
}

TypeScript: der RequestHandler-Type

Auch hier gibt es generierte $types für saubere Typisierung:

ts Typisierte Handler
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db';

export const GET: RequestHandler = async ({ params }) => {
    const produkt = await db.produkt.findUnique({
        where: { id: params.id },
    });
    if (!produkt) error(404);
    return json(produkt);
};

export const DELETE: RequestHandler = async ({ params, locals }) => {
    if (!locals.user) error(401);
    await db.produkt.delete({ where: { id: params.id } });
    return new Response(null, { status: 204 });
};

Vorteile: params.id ist als string typisiert, der Rückgabe-Typ wird gegen den erwarteten geprüft, IDE zeigt alle event-Felder im Auto-Vervollständigen.

Wann API Route, wann Form Action?

Beides verarbeitet Submits und gibt Antworten zurück. Die Wahl ist nicht beliebig — sie hängt davon ab, wer den Endpunkt aufruft.

SituationEmpfehlung
Klassisches HTML-Formular (mit <form method="POST">)Form Action
Aufruf von einer Mobile-App oder externem SystemAPI Route (+server.ts)
Webhook-EmpfängerAPI Route
Datenfluss innerhalb einer Page (Submit + neuer State)Form Action
GraphQL/Trpc-EndpunktAPI Route
Hintergrund-Job ruft per HTTP anAPI Route
Datei-Download (PDF, Excel, Bild)API Route

Form Actions gewinnen, wenn der Aufruf aus einer eigenen Page der App kommt: weniger Boilerplate (keine fetch-Aufrufe nötig), automatisches Progressive Enhancement, eingebaute Datenrückgabe an die Page. API Routes gewinnen, wenn der Aufruf von außerhalb kommt oder reines JSON liefern soll.

Antworten in verschiedenen Formaten

json(obj) ist der Standardfall. Aber der Endpunkt kann jede beliebige Antwort produzieren — du gibst einfach ein Response-Objekt zurück.

ts HTML zurückgeben
export function GET() {
    return new Response('<h1>Hallo</h1>', {
        headers: { 'Content-Type': 'text/html' },
    });
}
ts Datei-Download
export async function GET() {
    const buffer = await generatePdf();

    return new Response(buffer, {
        headers: {
            'Content-Type': 'application/pdf',
            'Content-Disposition': 'attachment; filename="report.pdf"',
        },
    });
}
ts Stream zurückgeben
export async function GET() {
    const stream = new ReadableStream({
        start(controller) {
            controller.enqueue('Erste Zeile\n');
            setTimeout(() => {
                controller.enqueue('Zweite Zeile\n');
                controller.close();
            }, 1000);
        },
    });

    return new Response(stream, {
        headers: { 'Content-Type': 'text/plain' },
    });
}

CORS-Header für externe Aufrufer

Wenn deine API von einer anderen Domain aufgerufen wird (z. B. von einer Mobile-App oder einem Drittanbieter-Frontend), brauchst du CORS-Header. Dafür ist die OPTIONS-Methode plus passende Header zuständig:

ts CORS-Konfiguration
import { json } from '@sveltejs/kit';

const ALLOW_ORIGIN = 'https://meine-mobile-app.example.com';

function corsHeaders() {
    return {
        'Access-Control-Allow-Origin': ALLOW_ORIGIN,
        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    };
}

export function OPTIONS() {
    return new Response(null, { headers: corsHeaders() });
}

export function GET() {
    return json({ data: '...' }, { headers: corsHeaders() });
}

Bei vielen Endpunkten lohnt es sich, das in einen Helper auszulagern oder im handle-Hook zentral zu setzen — siehe Hooks-Artikel.

Besonderheiten von API Routes

Ein paar Eigenheiten, die nicht selbstverständlich sind:

json(obj) ist optional, nicht Pflicht. Du kannst auch new Response(JSON.stringify(obj)) schreiben. Der Helper macht aber einen kleinen Unterschied — er setzt automatisch den Content-Type-Header und kürzt den Code. Praktisch genug, um ihn als Standard zu nehmen.

API Routes laufen in der gleichen Lifecycle wie Pages. Der handle-Hook (Auth, Logging, Header-Manipulation) läuft bei jedem API-Call genauso wie bei Page-Aufrufen. Deine Auth-Prüfung im handle schützt also automatisch alle API-Endpunkte — keine doppelte Logik nötig.

Beim internen event.fetch werden API Routes direkt aufgerufen. Wenn ein load oder eine Form Action event.fetch('/api/produkte') macht, ruft SvelteKit den +server.ts-Handler direkt auf — ohne echten HTTP-Roundtrip. Das ist deutlich schneller und überträgt Cookies automatisch.

Catch-All-Routes für Proxy-Patterns. Mit routes/api/[...path]/+server.ts kannst du alle API-Aufrufe abfangen und z. B. an ein Backend weiterleiten. Praktisch für Mono-Repo-Setups oder wenn du eine externe API hinter deiner eigenen Domain verstecken willst.

Ein Endpoint kann sowohl +server.ts als auch +page.svelte haben — aber nicht in derselben Route. Wenn du routes/produkte/+page.svelte hast (eine Seite) und gleichzeitig routes/produkte/+server.ts (eine API), führt SvelteKit zu einem Konflikt. Lege API-Routen unter /api/... ab oder einer anderen URL-Hierarchie.

Streamed-Responses für lange Operationen. Anders als Form Actions, die einmal antworten, kann ein +server.ts-Handler einen Stream zurückgeben (siehe oben) — gut für Server-Sent Events oder lange Berichte, deren Daten nach und nach kommen.

Error-Handler werden nicht eigenständig genutzt. Eine API-Route hat keine eigene +error.svelte. Wenn ein Endpoint mit error(...) antwortet, kriegt der Aufrufer eine Standard-JSON-Antwort mit der Error-Message. Eigene Fehler-Antworten formuliert man als JSON-Body selbst.

fallback für unbekannte Methoden. Wenn jemand mit PATCH an einen Endpoint geht, der nur GET und POST exportiert, antwortet SvelteKit standardmäßig mit 405 Method Not Allowed. Mit dem fallback-Export kannst du das selbst behandeln.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu SvelteKit Routing & Loading

Zur Übersicht