Gute REST-APIs: Ressourcen, Status-Codes, Versionierung

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Ressourcen als Substantive in der URL, die Aktion steckt in der HTTP-Methode:
GET /orders/42statt/getOrder?id=42.- Idempotenz ist praktisch: GET, PUT und DELETE darf man gefahrlos wiederholen, POST nicht.
- Wenige, ehrliche Status-Codes (200, 201, 204, 400, 401, 403, 404, 409, 422) und ein einheitliches Fehlerformat sparen Clients viel Rätselraten.
- Versionierung im Pfad ist plump, aber robust; Header sind eleganter, aber unbequemer. Paging und Dokumentation (z. B. Swagger) gehören von Anfang an dazu.
- HATEOAS ist die reine Lehre, im Alltag nur selten die Mühe wert.
In letzter Zeit habe ich mehrere Schnittstellen zu Gesicht bekommen, die sich REST nannten und es nur zur Hälfte waren. Eine hatte eine einzige URL, an die alles per POST geschickt wurde. Eine andere antwortete auf jeden Fehler mit Status 200 und einem Feld success: false im Body. Gebaut wurde beides mit guten Absichten. Ich halte REST nicht für ein Dogma, aber ein paar Konventionen machen eine Schnittstelle so viel angenehmer, dass es sich lohnt, sie ernst zu nehmen. Hier meine Sicht darauf, was sich bewährt hat.
Ressourcen statt Verben
Der erste Gedanke bei einer neuen Schnittstelle ist oft ein Satz Funktionen: getOrder, createOrder, cancelOrder. Das ist RPC mit HTTP als Transport, und es funktioniert auch. Der Gewinn von REST beginnt dort, wo man stattdessen in Dingen denkt. Eine Bestellung ist eine Ressource, sie hat eine Adresse, und was man mit ihr tut, sagt die Methode.
GET /orders Liste der Bestellungen
POST /orders neue Bestellung anlegen
GET /orders/42 eine Bestellung lesen
PUT /orders/42 Bestellung ersetzen
DELETE /orders/42 Bestellung löschen
GET /orders/42/items Positionen der BestellungSubstantive, Plural, keine Dateiendungen, keine Verben im Pfad. Wo eine Aktion sich partout nicht als Ressource ausdrücken lässt, etwa „Bestellung stornieren“, finde ich es besser, das pragmatisch zu lösen (ein Statusfeld per PATCH ändern oder eine Unterressource wie /orders/42/cancellation anlegen), als sich in Schönheit zu verbeißen. Ein Pfad, der ehrlich ein Verb enthält, ist besser als ein verbogenes Substantiv, das keiner versteht.
Methoden und Idempotenz
Der Teil, der im Alltag am meisten bringt, wird am seltensten erklärt: Idempotenz. Ein Aufruf ist idempotent, wenn ihn zweimal auszuführen denselben Zustand ergibt wie einmal. GET verändert nichts, PUT ersetzt eine Ressource komplett, DELETE löscht sie; alle drei darf ein Client nach einem Timeout unbesorgt wiederholen. POST gehört nicht dazu. Wenn die Antwort auf ein POST verloren geht, weiß der Client nicht, ob die Bestellung angelegt wurde. Wiederholt er, hat der Kunde zwei.
Das Netz ist unzuverlässig, und Clients wiederholen Anfragen, ob man es will oder nicht. Wer dort, wo es geht, idempotente Methoden wählt, erspart sich viel Ärger. Wo POST unvermeidlich ist, kann ein vom Client mitgeschickter Schlüssel helfen, doppelte Anfragen zu erkennen. PATCH für Teiländerungen ist standardisiert, wird aber noch nicht überall unterstützt, was man bei älteren Proxys und Frameworks im Hinterkopf behalten sollte.
Status-Codes, die etwas bedeuten
Der Status ist das Erste, was ein Client auswertet, bevor er den Body anschaut. Deshalb sollte er stimmen. Mir reicht meist ein kleiner Satz:
- 200 OK: es hat geklappt, im Body steht das Ergebnis.
- 201 Created: etwas wurde angelegt, im Header
Locationsteht die Adresse. - 204 No Content: geklappt, es gibt nichts zurückzugeben (typisch bei DELETE).
- 400 Bad Request: die Anfrage ist formal kaputt.
- 401 Unauthorized: nicht angemeldet (der Name ist unglücklich, gemeint ist Authentifizierung).
- 403 Forbidden: angemeldet, aber nicht berechtigt.
- 404 Not Found: gibt es nicht.
- 409 Conflict: kollidiert mit dem aktuellen Zustand, etwa eine veraltete Version oder ein doppelter Schlüssel.
- 422 Unprocessable Entity: formal in Ordnung, aber fachlich ungültig.
Die Grenze zwischen 400 und 422 ist Geschmackssache, und manche kommen mit 400 für beides aus. Wichtiger ist, überhaupt zu unterscheiden. Ein 200 mit Fehlermeldung im Body zwingt jeden Client, doppelt zu prüfen, und blendet nebenbei Caches, Monitoring und Proxys aus, die auf den Status schauen.
Fehlerformat und Paging
Zum Status gehört ein Body, der erklärt, was schiefging. Es ist weniger wichtig, wie genau er aussieht, als dass er überall gleich aussieht:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"code": "validation_failed",
"message": "Die Bestellung enthält ungültige Angaben.",
"details": [
{ "field": "items[0].quantity", "message": "muss größer als 0 sein" }
]
}Ein maschinenlesbarer Code, ein Text für Menschen und bei Validierungsfehlern die betroffenen Felder: Damit kommt man weit. Den Text zeigt der Client vielleicht an, auf den Code kann er sich verlassen.
Listen wachsen. Eine Schnittstelle, die heute zwanzig Einträge liefert, liefert in einem Jahr zwanzigtausend, und dann fällt auf, dass man nie ans Paging gedacht hat. Nachträglich ist es unangenehm einzuführen, weil sich das Antwortformat ändert. Ich plane es deshalb von Anfang an ein, etwa mit ?page=2&per_page=50 oder besser noch mit einem Cursor, wenn sich die Daten laufend ändern. In die Antwort gehören ein Hinweis auf die nächste Seite und, wenn es sich billig berechnen lässt, die Gesamtzahl.
Versionierung: Pfad oder Header
Eine Schnittstelle, die benutzt wird, wird sich ändern müssen, und dann darf man die bestehenden Clients nicht brechen. Zwei Wege sind üblich. Die Version im Pfad (/v1/orders) ist plump, aber jeder versteht sie auf den ersten Blick, man kann sie im Browser ausprobieren, in Logs sehen und im Routing trennen. Puristen führen an, dass sich die Adresse einer Ressource nicht mit der Version des Formats ändern sollte, und das stimmt in der Theorie. Der Weg über Header, entweder mit eigenem Media-Type wie application/vnd.example.v2+json im Accept-Header oder mit einem eigenen Feld, ist sauberer, aber unbequemer. Man kann ihn nicht einfach in die Adresszeile tippen, und manche Clients und Werkzeuge tun sich damit schwer.
Ich neige zum Pfad, weil Einfachheit für die Nutzer einer Schnittstelle schwerer wiegt als Reinheit. Wichtiger als die Technik finde ich die Haltung dahinter: Neue Felder hinzufügen ist fast immer unproblematisch, Felder entfernen oder umdeuten nie. Wer Clients anhält, unbekannte Felder zu ignorieren, braucht seltener eine neue Version, als man denkt.
HATEOAS und Dokumentation
Zur reinen Lehre von REST gehört, dass Antworten Links auf die nächsten sinnvollen Schritte enthalten, damit ein Client die Schnittstelle quasi von allein erkundet. Das Schlagwort dafür ist HATEOAS. Die Idee gefällt mir, und bei einfachen Dingen wie Paging-Links nutze ich sie gern. Als Prinzip durchgezogen habe ich sie selten gesehen und noch seltener als nötig empfunden. Clients werden in der Praxis gegen eine dokumentierte Struktur programmiert, nicht gegen eine entdeckte. Ich würde es als nützliche Ergänzung ansehen, nicht als Voraussetzung dafür, eine Schnittstelle REST nennen zu dürfen.
Die Dokumentation dagegen ist keine Kür. Eine Schnittstelle ohne gute Beschreibung ist für andere kaum zu benutzen. Mit Swagger, inzwischen in Version 2.0, lässt sich die Beschreibung maschinenlesbar pflegen und daraus eine Seite erzeugen, auf der man Aufrufe direkt ausprobieren kann. Das ist nicht nur schön für die Leser: Wer seine Schnittstelle beschreiben muss, entdeckt dabei Unstimmigkeiten, die vorher keinem aufgefallen sind.
Am Ende geht es bei all dem um Berechenbarkeit. Eine Schnittstelle ist gut, wenn man nach dem dritten Aufruf errät, wie der vierte aussieht.
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (Swagger-Logo: Fehguy, CC BY-SA 4.0; JSON-Logo: Douglas Crockford, gemeinfrei); die Marken gehören ihren Inhabern. Die Collage steht ebenfalls unter CC BY-SA 4.0.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.