APIs als langlebige Verträge
Eine API ist ein Vertrag. Sobald sie von einem Consumer genutzt wird, kann sie nicht mehr einfach geändert werden, zumindest nicht ohne Konsequenzen. In mehreren Projekten, von der öffentlichen Verwaltung bis zum Banking, habe ich erlebt, wie teuer schlechtes API-Design im Nachhinein wird: Breaking Changes, die monatelange Migrationen auslösen. Inkonsistente Fehlerformate, die jeden Consumer eigene Workarounds entwickeln lassen. Fehlende Pagination, die bei wachsenden Datenmengen zum Problem wird.
Gutes API-Design beginnt nicht beim ersten Endpoint, sondern bei den Prinzipien, die über die gesamte Lebensdauer der API gelten sollen.
Contract-First mit OpenAPI
Der wichtigste Paradigmenwechsel im API-Design ist der Wechsel von Code-First zu Contract-First. Statt zuerst den Code zu schreiben und dann eine Dokumentation zu generieren, wird zuerst der Vertrag in Form einer OpenAPI-Spezifikation definiert.
openapi: 3.0.3
info:
title: Antragsverwaltung API
version: 1.2.0
paths:
/antraege:
get:
summary: Liste aller Anträge
parameters:
- name: status
in: query
schema:
type: string
enum: [eingegangen, in_bearbeitung, abgeschlossen]
- name: page
in: query
schema:
type: integer
default: 0
- name: size
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Paginierte Liste der Anträge
content:
application/json:
schema:
$ref: '#/components/schemas/AntragPage'
Der Vorteil ist offensichtlich: Der Vertrag kann reviewed werden, bevor eine Zeile Code geschrieben wird. Frontend- und Backend-Teams können parallel arbeiten. Consumer können Mock-Server auf Basis der Spezifikation aufsetzen. Und die Spezifikation ist die Single Source of Truth, nicht der Code und nicht die Dokumentation.
Versionierung: Von Anfang an mitdenken
Es gibt drei gängige Strategien für API-Versionierung. Jede hat Vor- und Nachteile:
| Strategie | Beispiel | Vorteile | Nachteile |
|---|---|---|---|
| URL-Pfad | /v1/antraege | Einfach, explizit, cachebar | URL-Änderung bei jeder Version |
| Header | Accept: application/vnd.api.v1+json | Saubere URLs | Schwerer zu testen, weniger sichtbar |
| Query-Parameter | /antraege?version=1 | Flexibel | Nicht RESTful im engeren Sinne |
In der Praxis hat sich die URL-Pfad-Versionierung durchgesetzt. Sie ist am einfachsten zu verstehen, zu testen und zu dokumentieren. Die theoretische Eleganz der Header-Versionierung steht in keinem Verhältnis zu den praktischen Problemen, die sie in der Kommunikation mit Consumern verursacht.
Entscheidend ist: Die Versionierungsstrategie muss vor dem ersten Release festgelegt werden. Eine nachträgliche Einführung ist mit erheblichem Aufwand verbunden.
HATEOAS: Pragmatische Einschätzung
HATEOAS, Hypermedia as the Engine of Application State, das klingt elegant. Die Idee: Eine API liefert mit jeder Response Links mit, die dem Consumer sagen, welche Aktionen als nächstes möglich sind. In der Theorie ermöglicht das selbstentdeckende Clients, die sich an Änderungen der API anpassen, ohne neu programmiert werden zu müssen.
In der Praxis sieht es anders aus. Nach meiner Erfahrung lohnt sich HATEOAS in voller Ausprägung nur für APIs mit vielen möglichen Zustandsübergängen und generischen Clients. Für die typische Enterprise-API, die von einem oder zwei bekannten Consumern genutzt wird, ist der Overhead nicht gerechtfertigt.
Ein pragmatischer Mittelweg: Links für Navigation (z.B. Pagination) und offensichtliche Relationen bereitstellen, aber nicht den gesamten Zustandsautomaten in jeder Response abbilden.
{
"data": [...],
"links": {
"self": "/v1/antraege?page=2&size=20",
"next": "/v1/antraege?page=3&size=20",
"prev": "/v1/antraege?page=1&size=20"
},
"page": {
"number": 2,
"size": 20,
"totalElements": 458,
"totalPages": 23
}
}
Fehlerbehandlung: Konsistenz ist Pflicht
Inkonsistente Fehlermeldungen sind einer der häufigsten Beschwerden von API-Consumern. Jeder Endpoint liefert Fehler in einem anderen Format, manche als String, manche als verschachteltes JSON, manche mit HTTP-Status 200 und einem Fehlerfeld im Body.
Ein konsistentes Fehlerformat über alle Endpoints hinweg ist nicht verhandelbar:
{
"error": {
"code": "ANTRAG_NOT_FOUND",
"message": "Antrag mit ID 12345 nicht gefunden",
"timestamp": "2020-07-22T14:30:00Z",
"details": [
{
"field": "antragId",
"message": "Kein Antrag mit dieser ID vorhanden"
}
]
}
}
Die Regeln:
- HTTP-Statuscodes korrekt verwenden: 400 für Client-Fehler, 404 für nicht gefundene Ressourcen, 422 für Validierungsfehler, 500 für Server-Fehler
- Maschinenlesbare Fehlercodes: Nicht nur eine Nachricht, sondern einen Code, den der Consumer programmatisch auswerten kann
- Keine Stack Traces in Produktion: Interne Fehlerdetails gehören ins Log, nicht in die Response
Idempotenz: Sichere Wiederholbarkeit
In verteilten Systemen können Requests verloren gehen oder doppelt gesendet werden. Idempotente Endpoints stellen sicher, dass eine mehrfache Ausführung desselben Requests dasselbe Ergebnis liefert.
GET, PUT und DELETE sind per Definition idempotent. Für POST, typischerweise die Erstellung einer Ressource, empfiehlt sich ein Idempotency Key:
POST /v1/zahlungen
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"empfaenger": "DE89370400440532013000",
"betrag": 150.00,
"waehrung": "EUR"
}
Der Server speichert den Idempotency Key zusammen mit der Response. Bei einem erneuten Request mit demselben Key wird die gespeicherte Response zurückgeliefert, ohne die Operation erneut auszuführen. Für Zahlungssysteme ist das keine optionale Nettigkeit, sondern eine Pflichtanforderung.
Pagination: Nicht verhandelbar
Jeder Endpoint, der Listen zurückliefert, braucht Pagination. Ausnahmslos. Auch wenn die Liste heute nur 50 Einträge hat, sie wird wachsen. Pagination nachträglich einzuführen ist ein Breaking Change.
Cursor-basierte Pagination hat sich gegenüber Offset-basierter Pagination als robuster erwiesen, besonders bei großen Datenmengen und häufigen Änderungen. Aber auch einfache Offset-Pagination ist besser als keine Pagination.
Fazit
Gutes API-Design ist eine Investition, die sich über die gesamte Lebensdauer einer Schnittstelle auszahlt. Contract-First mit OpenAPI, eine klare Versionierungsstrategie, konsistente Fehlerformate und eingebaute Idempotenz, diese Prinzipien kosten in der Initialentwicklung etwas mehr Aufwand, ersparen aber ein Vielfaches an Migrationskosten und Frust bei den Consumern. Eine API ist ein Versprechen. Und Versprechen sollte man halten.