REST API Design: Best Practices für URLs, Fehler, Pagination und Versionierung
REST API Design nach RFC 9110 und 9457: Methoden, Statuscodes, Fehlerformat, Pagination, Idempotenz und Versionierung, mit Beispielen von GitHub.
Thema: Web-Entwicklung
Inhalt (12 Abschnitte)
Gutes REST API Design heißt: Ressourcen als Substantive in der URL, HTTP-Methoden und Statuscodes so verwenden, wie RFC 9110 sie definiert, Fehler einheitlich nach RFC 9457 melden, Listen von Anfang an seitenweise ausliefern und eine Versionierungsregel festlegen, bevor der erste Client existiert. Genau diese Entscheidungen lassen sich später kaum noch ändern.
Stand: 13. Oktober 2026. Die Angaben stammen aus den RFCs der IETF, der Dissertation von Roy Fielding, den API-Richtlinien von Google, Microsoft und Zalando, der Dokumentation von GitHub, Stripe und PostgreSQL sowie der OpenAPI-Spezifikation, abgerufen an diesem Tag. Die Quellen stehen am Ende.
Was REST bedeutet
REST steht für Representational State Transfer. Roy Fielding hat den Architekturstil in seiner Dissertation aus dem Jahr 2000 beschrieben, als Satz von Einschränkungen: Client und Server getrennt, zustandslose Kommunikation, zwischenspeicherbare Antworten, eine einheitliche Schnittstelle, ein geschichtetes System und optional Code auf Abruf. Die einheitliche Schnittstelle definiert er durch vier Bedingungen: Ressourcen sind identifizierbar, sie werden über Repräsentationen verändert, Nachrichten beschreiben sich selbst, und Hypermedia steuert den Zustand der Anwendung.
Was im Alltag „REST API“ heißt, ist meist eine HTTP-Schnittstelle mit JSON, die einen Teil dieser Bedingungen erfüllt. Für das Design zählt nach Einschätzung weniger die reine Lehre als zwei Fragen: Hält sich die API an die Bedeutung, die HTTP selbst vorgibt? Und bleibt sie für bestehende Clients stabil, wenn sie wächst?
Ressourcen und URLs benennen
URLs bezeichnen Ressourcen, die Methode sagt, was mit ihnen geschieht. Eine verbreitete Konvention, hier als Beispiel:
| Aufruf | Bedeutung |
|---|---|
GET /kunden | Liste der Kunden |
POST /kunden | neuen Kunden anlegen |
GET /kunden/42 | einen Kunden lesen |
PATCH /kunden/42 | einen Kunden teilweise ändern |
DELETE /kunden/42 | einen Kunden löschen |
GET /kunden/42/rechnungen | Rechnungen dieses Kunden |
Verben wie /kundeAnlegen oder /getKunde gehören nach dieser Konvention nicht in die URL. Für Schreibweisen gibt es keine Norm, wohl aber Richtlinien großer Anbieter. Die Zalando RESTful API Guidelines etwa schreiben kebab-case für Pfadsegmente und snake_case für Query-Parameter und Feldnamen vor. Wichtiger als die gewählte Schreibweise ist nach Einschätzung, dass sie in der ganzen API gleich bleibt.
HTTP-Methoden richtig einsetzen
RFC 9110 legt fest, welche Methoden „sicher“ und welche „idempotent“ sind. Sicher heißt: Der Client verlangt keine Zustandsänderung auf dem Server. Idempotent heißt: Mehrere gleiche Anfragen haben dieselbe beabsichtigte Wirkung wie eine einzige. Das ist mehr als Theorie: Idempotente Anfragen darf ein Client laut RFC nach einem Verbindungsfehler automatisch wiederholen.
| Methode | typischer Zweck | sicher | idempotent |
|---|---|---|---|
| GET | lesen | ja | ja |
| HEAD | wie GET, nur Kopfzeilen | ja | ja |
| POST | anlegen, Aktion auslösen | nein | nein |
| PUT | Ressource vollständig ersetzen oder anlegen | nein | ja |
| PATCH | Ressource teilweise ändern (RFC 5789) | nein | nein |
| DELETE | löschen | nein | ja |
Daraus folgen zwei Regeln: Ein GET darf nichts verändern, denn Crawler und Caches verlassen sich laut RFC darauf, dass sichere Methoden keinen Schaden anrichten. Und ein POST, der etwas Wichtiges auslöst, braucht einen eigenen Schutz gegen doppelte Ausführung (siehe Idempotency-Key weiter unten).
Statuscodes, die eine API wirklich braucht
Die meisten APIs kommen mit wenigen Codes aus. Die Bedeutungen stammen aus RFC 9110, der Code 429 aus RFC 6585:
| Code | Bedeutung laut RFC | typischer Einsatz |
|---|---|---|
| 200 OK | Anfrage erfolgreich | Lesen, Ändern mit Antwortinhalt |
| 201 Created | neue Ressource angelegt, Adresse im Location-Header | erfolgreicher POST |
| 204 No Content | erfolgreich, kein Inhalt | DELETE |
| 400 Bad Request | Client-Fehler, etwa fehlerhafte Syntax | kaputtes JSON |
| 401 Unauthorized | gültige Anmeldedaten fehlen; Server muss WWW-Authenticate senden | fehlendes oder abgelaufenes Token |
| 403 Forbidden | Anfrage verstanden, Ausführung verweigert | angemeldet, aber ohne Recht |
| 404 Not Found | Ressource nicht gefunden | falsche ID |
| 409 Conflict | Konflikt mit dem aktuellen Zustand | E-Mail-Adresse schon vergeben |
| 412 Precondition Failed | Vorbedingung im Header nicht erfüllt | If-Match mit veraltetem ETag |
| 422 Unprocessable Content | Syntax korrekt, Inhalt nicht verarbeitbar | Validierungsfehler |
| 429 Too Many Requests | zu viele Anfragen in einer Zeitspanne; Retry-After möglich | Rate Limit |
| 500 Internal Server Error | unerwarteter Fehler auf dem Server | Ausnahme im Code |
| 503 Service Unavailable | vorübergehend nicht verfügbar | Wartung, Überlast |
Als Merkhilfe für 401 und 403: 401 heißt „Anmeldung fehlt oder ist ungültig“, 403 heißt „angemeldet, aber nicht berechtigt“. Wie du Anmeldung und Rechte sauber umsetzt, steht in Web Security für Entwickler.
Fehler einheitlich melden: Problem Details nach RFC 9457
Ohne festes Fehlerformat baut nach Einschätzung jeder Endpoint sein eigenes, und jeder Client braucht für jeden Endpoint eigenen Fehlercode. RFC 9457 „Problem Details for HTTP APIs“ (Juli 2023, ersetzt RFC 7807) definiert dafür ein JSON-Format mit dem Medientyp application/problem+json. Ein Beispiel nach dem Muster der RFC:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/probleme/validierung",
"title": "Die Anfrage ist ungültig.",
"status": 422,
"errors": [
{ "detail": "muss eine positive ganze Zahl sein", "pointer": "#/menge" },
{ "detail": "muss 'rot', 'grün' oder 'blau' sein", "pointer": "#/farbe" }
]
}
| Feld | Bedeutung laut RFC 9457 |
|---|---|
type | URI, die die Art des Problems bezeichnet; fehlt es, gilt about:blank |
title | kurze, für Menschen lesbare Zusammenfassung der Problemart |
status | der HTTP-Statuscode, als Hinweis für den Client |
detail | Erklärung zu genau diesem Vorfall; soll beim Beheben helfen, nicht beim Debuggen |
instance | URI-Referenz auf genau diesen Vorfall |
Eigene Felder wie errors sind als Erweiterungen erlaubt. Clients sollen laut RFC detail nicht maschinell auswerten; dafür sind Erweiterungsfelder da. Aus den Sicherheitshinweisen der RFC: Keine Implementierungsdetails wie Stack-Traces über die Schnittstelle ausgeben. Zalando schreibt Problem JSON in seinen Richtlinien verpflichtend vor.
Pagination von Anfang an
Googles API-Richtlinie AIP-158 ist hier eindeutig: Methoden, die Sammlungen liefern, müssen von Beginn an seitenweise ausliefern, weil das nachträgliche Einführen eine inkompatible Änderung ist. Ein Client, der heute alle Einträge in einer Antwort erwartet, bricht, sobald plötzlich nur noch die ersten 50 kommen.
Es gibt zwei verbreitete Verfahren:
| Verfahren | Beispiel | Vorteil | Nachteil |
|---|---|---|---|
| Offset | ?limit=50&offset=100 | einfach, Sprung auf Seite n möglich | neue Einträge verschieben die Seiten, große Offsets werden langsam |
| Cursor | ?limit=50&starting_after=obj_123 | stabil bei neuen Einträgen, gleichmäßig schnell | kein Sprung auf Seite n |
Stripe nutzt Cursor: Listen-Endpoints nehmen limit (1 bis 100, Standard 10) sowie starting_after oder ending_before mit einer Objekt-ID. Google gibt in AIP-158 ein next_page_token zurück, das am Ende der Liste leer ist. GitHub liefert die Adressen der nächsten, vorigen, ersten und letzten Seite im link-Header nach RFC 8288.
Ein Cursor sollte für den Client undurchsichtig sein, damit du die Umsetzung später ändern kannst. Ein Beispiel in Node.js, das den Cursor aus Zeitstempel und ID des letzten Eintrags bildet:
// cursor.mjs
// Der Cursor ist für den Client undurchsichtig: Base64url-kodiertes JSON
export function cursorErzeugen({ erstellt, id }) {
return Buffer.from(JSON.stringify({ erstellt, id })).toString('base64url');
}
export function cursorLesen(cursor) {
const { erstellt, id } = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
if (typeof erstellt !== 'string' || !Number.isInteger(id)) {
throw new Error('ungültiger Cursor');
}
return { erstellt, id };
}
Die passende Abfrage nutzt einen Zeilenvergleich, den PostgreSQL von links nach rechts auswertet. Die ID als zweites Kriterium sorgt dafür, dass Einträge mit gleichem Zeitstempel weder doppelt noch gar nicht erscheinen:
SELECT id, erstellt FROM bestellungen
WHERE (erstellt, id) < ($1, $2)
ORDER BY erstellt DESC, id DESC
LIMIT $3;
Modul und Abfrage wurden mit Node.js 22.22 gegen eine In-Memory-Datenbank in SQLite geprüft, die denselben Zeilenvergleich kennt: Sieben Einträge, davon vier mit gleichem Zeitstempel, kamen in Seiten zu je drei vollständig und ohne Doppelte. Speichert PostgreSQL Mikrosekunden, liefern Node-Treiber wie node-postgres und PGlite timestamptz als Date, das nur Millisekunden kennt; die Mikrosekunden fehlen dann im Cursor, und Einträge mit fast gleichem Zeitstempel können übersprungen werden. Gegen PostgreSQL in PGlite 0.5 fehlte so bei zwei Zeitstempeln mit Mikrosekunden ein Eintrag. Gib den Zeitstempel für den Cursor deshalb zusätzlich als Text aus (erstellt::text AS cursor_zeit) oder lege die Spalte als timestamptz(3) an; mit beiden Varianten kamen alle sieben Einträge. Für große Tabellen braucht sie einen Index auf (erstellt, id); wie weit PostgreSQL für solche Aufgaben trägt, steht in Warum Postgres fast immer reicht.
Doppelte Zahlungen verhindern: der Idempotency-Key
POST ist nicht idempotent. Bricht die Verbindung nach dem Absenden ab, weiß der Client nicht, ob die Bestellung angelegt wurde. Wiederholt er die Anfrage, entsteht womöglich eine zweite. Stripe löst das mit dem Header Idempotency-Key: Der Client erzeugt einen eindeutigen Schlüssel, Stripe empfiehlt UUIDs der Version 4, und schickt ihn bei jeder Wiederholung mit. In der API v1 speichert Stripe Statuscode und Antwort des ersten Aufrufs und gibt sie bei jeder Wiederholung mit demselben Schlüssel zurück, auch einen Fehler 500.
POST /v1/bestellungen HTTP/1.1
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-3b4d-4c8e-9a71-2f5e8d0b4c13
Stripe nennt dazu Details, die beim Nachbauen helfen: Schlüssel bis 255 Zeichen, keine sensiblen Daten im Schlüssel, in der API v1 Löschen frühestens nach 24 Stunden, und ein Fehler, wenn derselbe Schlüssel mit anderen Parametern kommt. Ein Standard ist der Header bisher nicht: Der IETF-Entwurf „The Idempotency-Key HTTP Header Field“ ist nach Revision 07 abgelaufen.
Versionierung und Abkündigung
Es gibt keine einheitliche Regel, aber jede große Richtlinie legt sich fest:
| Richtlinie | Verfahren |
|---|---|
| Google (AIP-185) | Hauptversion als erster Teil des Pfads, etwa /v1/; keine Neben- oder Patchversionen |
| Microsoft Azure | Pflicht-Query-Parameter api-version mit Datum YYYY-MM-DD; fehlt er, Fehler 400 |
| GitHub | Header X-GitHub-Api-Version mit Datum; ohne Header gilt 2022-11-28; alte Versionen mindestens 24 Monate nach Erscheinen der neuen unterstützt |
| Zalando | Versionierung möglichst vermeiden; wenn nötig über den Medientyp, nie in der URL |
GitHub listet auch, was als inkompatible Änderung gilt: unter anderem einen Endpoint entfernen, einen Parameter oder ein Antwortfeld entfernen oder umbenennen, einen neuen Pflichtparameter einführen, den Typ eines Feldes ändern und Enum-Werte entfernen. Als kompatibel gelten neue Endpoints, optionale Parameter und neue Antwortfelder. Clients sollten unbekannte Felder deshalb nach Einschätzung ignorieren, statt daran zu scheitern.
Für das Abkündigen gibt es zwei Header: Deprecation nach RFC 9745 (März 2025) kündigt an, dass eine Ressource veraltet ist oder sein wird, Sunset nach RFC 8594 nennt den Zeitpunkt, ab dem sie voraussichtlich nicht mehr antwortet. GitHub antwortet nach Ablauf der Unterstützung mit 410 Gone.
Dokumentation mit OpenAPI
Die OpenAPI Specification beschreibt HTTP-APIs in einem standardisierten, sprachunabhängigen Format, das Menschen und Programme lesen können; die aktuelle Fassung ist 3.2.1 vom 10. September 2026. Laut Spezifikation nutzen Werkzeuge eine solche Beschreibung, um Dokumentation anzuzeigen, Server und Clients in verschiedenen Sprachen zu erzeugen und zu testen. Nach Einschätzung lohnt es sich, die Datei vor dem Code zu schreiben: Dann entstehen Namen, Fehlerformat und Pagination einmal für die ganze API statt je Endpoint.
Checkliste vor dem ersten Client
| Entscheidung | Festlegen vor dem ersten Client |
|---|---|
| Schreibweise von Pfaden und Feldern | ja, später nur mit neuer Version änderbar |
| Fehlerformat (RFC 9457) | ja |
| Pagination für jede Liste | ja, nachträglich inkompatibel |
| Versionierungsverfahren | ja |
| Idempotency-Key für kritische POSTs | ja, wenn Geld oder Bestellungen betroffen sind |
Rate Limits mit 429 und Retry-After | sinnvoll, sobald die API öffentlich ist |
Ob du Anmeldung selbst baust oder einen Dienst nutzt, ist eine eigene Entscheidung; fertige Backends mit eingebauter Anmeldung vergleicht der Überblick Supabase-Alternativen.
Quellen und Stand
Alle Angaben wurden am 13. Oktober 2026 geprüft an:
- Roy Fielding: Representational State Transfer (REST), Kapitel 5 der Dissertation
- IETF: RFC 9110 HTTP Semantics, RFC 5789 PATCH, RFC 6585 (429), RFC 9457 Problem Details, RFC 8288 Web Linking, RFC 9745 Deprecation, RFC 8594 Sunset
- IETF Datatracker: Entwurf Idempotency-Key
- Google: AIP-158 Pagination und AIP-185 API Versioning
- Microsoft: Azure REST API Guidelines
- Zalando RESTful API Guidelines
- GitHub: Pagination und API-Versionen
- Stripe: Idempotent requests und Pagination
- PostgreSQL: Row Constructor Comparison
- OpenAPI Specification
Häufige Fragen
Was sind REST API Best Practices?
Ressourcen als Substantive in der URL, Methoden und Statuscodes nach RFC 9110, ein einheitliches Fehlerformat nach RFC 9457, Pagination für jede Liste von Anfang an, Schutz vor doppelten POSTs per Idempotency-Key und eine feste Versionierungsregel. Dazu eine Beschreibung in OpenAPI, damit alle Endpoints denselben Regeln folgen.
Wie benennt man Endpoints in einer REST API?
Mit Substantiven im Plural für Sammlungen und der ID für einzelne Einträge, etwa /kunden und /kunden/42; die Aktion steckt in der HTTP-Methode. Eine Norm für die Schreibweise gibt es nicht. Zalando etwa verlangt kebab-case in Pfaden und snake_case in Feldnamen. Entscheidend ist, eine Schreibweise durchzuhalten.
Wie sollte eine REST API Fehler zurückgeben?
Mit einem passenden Statuscode und einem einheitlichen Inhalt im Format application/problem+json nach RFC 9457: type, title, status, detail und instance, ergänzt um eigene Felder wie eine Liste von Validierungsfehlern. Stack-Traces und andere Interna gehören nicht in die Antwort.
Wie versioniert man eine REST API?
Üblich sind drei Wege: Version im Pfad wie /v1/ (Google), als Query-Parameter mit Datum wie api-version=2026-10-13 (Microsoft) oder im Header (GitHub). Wichtiger als der Weg ist, festzulegen, was als inkompatible Änderung gilt, und alte Versionen mit Deprecation- und Sunset-Header rechtzeitig abzukündigen.
Wie funktioniert Pagination in einer REST API?
Der Client fragt eine begrenzte Zahl von Einträgen ab und bekommt einen Verweis auf die nächste Seite, etwa einen Cursor, ein next_page_token oder einen link-Header. Cursor-Pagination bleibt stabil, wenn neue Einträge hinzukommen. Google verlangt Pagination von Beginn an, weil sie sich nicht kompatibel nachrüsten lässt.
Was ist der Unterschied zwischen PUT und PATCH?
PUT ersetzt eine Ressource vollständig und ist laut RFC 9110 idempotent: Zwei gleiche PUTs haben dieselbe Wirkung wie einer. PATCH ändert nur Teile und ist laut RFC 5789 weder sicher noch idempotent. Für Teiländerungen ist PATCH deshalb die passende Methode, bei Wiederholungen aber vorsichtig zu behandeln.