API-Design: Schnittstellen entwerfen, die man nicht bereut

API-Design: Schnittstellen entwerfen, die man nicht bereut

API-Design: Schnittstellen entwerfen, die man nicht bereut

Artikel-Übersicht

Eine Schnittstelle ist ein Versprechen an alle, die sie benutzen — und anders als bei internem Code kann man es nicht einseitig zurücknehmen. Dieser Leitfaden behandelt deshalb die Entscheidungen, die am teuersten sind, wenn sie schiefgehen. Er beginnt mit API-first als Vorgehen, geht dann durch den Ressourcenschnitt, die richtige Verwendung von HTTP-Methoden und Statuscodes und ein einheitliches Fehlerformat. Der ausführlichste Abschnitt gilt der Versionierung, weil dort die häufigste Fehleinschätzung sitzt: Die meisten Teams wissen nicht genau, was überhaupt eine brechende Änderung ist. Es folgen Paginierung mit dem Argument für Cursor statt Offset, Authentifizierung samt der Frage, wie man einen Schlüssel sicher erzeugt und speichert, Schnittstellen zwischen Diensten und ein kurzer Vergleich von REST, GraphQL und gRPC.

Was eine Schnittstelle ist

Eine Schnittstelle ist ein Vertrag: Sie sagt zu, welche Anfragen entgegengenommen werden und was daraufhin zurückkommt.

Der deutsche Begriff Schnittstelle trifft es dabei besser als das englische Kürzel, weil er die Sache benennt — eine Stelle, an der zwei Systeme aneinandergrenzen und sich auf etwas einigen müssen. In Lastenheften und Ausschreibungen steht deshalb fast immer „Schnittstelle" und selten „API".

Der Unterschied zu internem Code ist grundlegend: Eine Klasse kannst du umbenennen, weil du alle Aufrufer kennst. Eine veröffentlichte Schnittstelle kannst du nicht ändern, weil du sie nicht kennst. Alles, was in diesem Artikel folgt, ergibt sich aus dieser einen Asymmetrie.

API-first: die Spezifikation zuerst

Der übliche Weg ist, die Schnittstelle beim Implementieren entstehen zu lassen und hinterher zu dokumentieren. Das erzeugt Schnittstellen, die die innere Struktur nach außen spiegeln — und die sich mit jeder Umstrukturierung mitändern.

API-first dreht die Reihenfolge um: Erst wird die Schnittstelle als Spezifikation geschrieben und abgestimmt, dann implementiert. Drei Dinge werden dadurch möglich:

  • Prüfung vor Implementierung. Ein Entwurf lässt sich in einer halben Stunde durchsprechen. Eine fertige Schnittstelle zu ändern kostet Tage.
  • Parallele Arbeit. Anbieter und Nutzer können gleichzeitig anfangen, weil beide wissen, worauf sie sich verlassen können.
  • Generierte Clients. Aus der Spezifikation lassen sich Client-Bibliotheken und Servergerüste erzeugen — das spart nicht nur Tipparbeit, es garantiert auch, dass beide Seiten dasselbe verstehen. Wie das praktisch geht, steht in OpenAPI Generator: Client und Server aus der Spezifikation erzeugen.

Der Preis ist Disziplin: Wenn die Spezifikation nach der ersten Woche nicht mehr gepflegt wird, hat man zwei Wahrheiten statt einer.

Ressourcen schneiden

REST denkt in Dingen, nicht in Vorgängen. Die Regeln:

  • Substantive im Plural, keine Verben. /rechnungen, nicht /getRechnung oder /rechnungErstellen.
  • Hierarchie nur, wo sie trägt. /rechnungen/{id}/positionen ist sinnvoll, wenn eine Position ohne ihre Rechnung nicht existiert. /kunden/{id}/rechnungen/{id}/positionen/{id} ist eine Zumutung — ab der zweiten Ebene lieber eine eigene Ressource mit Filter.
  • Ein Bezeichner, ein Ding. Wenn dieselbe Ressource unter zwei Pfaden erreichbar ist, wird eine davon irgendwann anders behandelt.

Und die Ausnahme, um die man nicht herumkommt: Aktionen, die keine Ressource sind. Ein Passwort zurücksetzen, eine Rechnung stornieren, einen Auftrag freigeben. Zwei saubere Wege:

POST /rechnungen/{id}/stornierung      ← die Aktion als Ressource modellieren
PATCH /rechnungen/{id}   { "status": "storniert" }   ← Zustandsänderung

Die erste Variante ist meist die bessere, weil sie einen eigenen Rückgabewert und eigene Fehler haben darf. Was man vermeiden sollte, ist ein POST /stornierenRechnung — das ist eine Fernprozedur mit HTTP-Anstrich.

HTTP richtig benutzen

Die Methoden tragen Zusicherungen, die Clients und Zwischenschichten auswerten:

Methode Sicher (ändert nichts) Idempotent (mehrfach = einmal) Wofür
GET ja ja lesen
HEAD ja ja prüfen ohne Inhalt
PUT nein ja vollständig ersetzen
DELETE nein ja löschen
PATCH nein nein teilweise ändern
POST nein nein erzeugen, alles Übrige

Dass PUT und DELETE idempotent sind, ist keine Formalie: Ein Client, dessen Aufruf in eine Zeitüberschreitung läuft, darf ihn wiederholen. Bei POST darf er das nicht — dafür braucht es einen Idempotenzschlüssel.

Und die Statuscodes, mit dem, was der Client daraus ableiten soll:

Code Bedeutung Was der Client tun soll
200 erfolgreich, Inhalt folgt weiterverarbeiten
201 erzeugt Ort aus dem Location-Feld merken
204 erfolgreich, kein Inhalt nichts erwarten
400 Anfrage fehlerhaft Anfrage korrigieren, nicht wiederholen
401 nicht angemeldet Anmeldung erneuern
403 angemeldet, aber nicht berechtigt nicht erneut anmelden
404 nicht vorhanden aufgeben oder anlegen
409 Konflikt mit dem aktuellen Zustand Zustand neu laden, dann erneut
422 verstanden, fachlich abgelehnt fachlichen Fehler anzeigen
429 zu viele Anfragen warten, Retry-After beachten
5xx Serverfehler mit Backoff wiederholen

Der häufigste Fehler ist, alles mit 200 zu beantworten und den Fehler in den Rumpf zu legen. Damit funktioniert keine Zwischenschicht mehr: Kein Proxy erkennt den Fehler, keine Wiederholungslogik greift, keine Überwachung schlägt an. Die Fehlerrate im Dashboard bleibt bei null, während die Anwendung reihenweise scheitert.

Die Unterscheidung zwischen 400 und 422 lohnt: 400 heißt, die Anfrage war schon syntaktisch falsch. 422 heißt, sie war formal in Ordnung, aber fachlich nicht zulässig — etwa eine Stornierung einer bereits stornierten Rechnung.

Fehler zurückgeben

Ein einheitliches Fehlerformat für die gesamte Schnittstelle, maschinenlesbar:

{
  "type": "https://api.beispiel.de/fehler/rechnung-bereits-storniert",
  "title": "Rechnung bereits storniert",
  "status": 422,
  "detail": "Die Rechnung R-2026-00417 wurde am 12.11.2026 storniert.",
  "instance": "/rechnungen/R-2026-00417/stornierung",
  "fehlercode": "RECHNUNG_BEREITS_STORNIERT",
  "korrelationsId": "9f2c1e44-8b2a-4a1f-9c1d-2b7e5a3f0c11"
}

Vier Eigenschaften machen den Unterschied:

  • Ein stabiler Fehlercode zum Auswerten. Menschenlesbare Texte ändern sich, Codes dürfen es nicht.
  • Eine Korrelations-ID, damit der Anrufer beim Support eine Nummer nennen kann, die in deinen Protokollen auffindbar ist.
  • Keine internen Details. Kein Stacktrace, keine SQL-Anweisung, keine Klassennamen. Das ist eine Sicherheitsfrage.
  • Dasselbe Format überall. Auch bei 500, auch beim Rahmenwerk-Fehler. Sonst muss jeder Client zwei Varianten behandeln.

Für das Format gibt es einen etablierten Standard — Problem Details for HTTP APIs —, den man übernehmen und um eigene Felder ergänzen kann. Selbst erfinden lohnt nicht.

Versionierung

Die teuerste Entscheidung des ganzen Artikels, und sie beginnt mit einer Frage, die viele nicht sauber beantworten können.

Was ist überhaupt eine brechende Änderung?

Änderung Brechend?
Neues optionales Feld in der Antwort nein
Neues Pflichtfeld in der Anfrage ja
Feld aus der Antwort entfernen ja
Feld umbenennen ja — das ist Entfernen plus Hinzufügen
Datentyp ändern (Zahl → Zeichenkette) ja
Neuer Wert in einer Aufzählung ja, in der Praxis — Clients mit switch scheitern
Reihenfolge in einem Array ändern ja, wenn sie zugesagt war
Feld optional machen, das Pflicht war nein für Sender, ja für Empfänger
Statuscode für denselben Fall ändern ja
Fehlerformat ändern ja

Die Zeile mit der Aufzählung ist die, die am häufigsten überrascht. Formal ist ein neuer Wert additiv — praktisch bricht er jeden Client, der alle bekannten Werte behandelt und sonst eine Ausnahme wirft. Gegenmittel: In der Spezifikation von Anfang an festhalten, dass Aufzählungen erweiterbar sind und unbekannte Werte toleriert werden müssen.

Wie versionieren?

Weg Beispiel Vorteil Nachteil
Pfad /v1/rechnungen sofort sichtbar, leicht zu routen die Version steht in jedem Bezeichner
Header Accept: application/vnd.firma.v2+json Bezeichner bleiben stabil unsichtbar, schwer zu testen, Caching-Fallen
Parameter /rechnungen?version=2 einfach vermischt Version und Abfrage

Meine Empfehlung ist der Pfad, aus einem pragmatischen Grund: Man sieht die Version in jedem Protokolleintrag, in jedem curl-Aufruf und in jeder Fehlermeldung. Die theoretische Reinheit des Headers wiegt das nicht auf.

Wichtiger als die Wahl ist aber die Haltung: Die beste Versionierung ist die, die man nicht braucht. Additive Änderungen brauchen keine neue Version. Wer von Anfang an so entwirft, dass Erweiterungen möglich sind — optionale Felder, tolerante Clients, erweiterbare Aufzählungen —, kommt jahrelang mit v1 aus.

Paginierung, Filtern, Sortieren

Für Paginierung gibt es zwei Wege, und einer davon ist in den meisten Fällen falsch.

Offset (?offset=40&limit=20) ist einfach und bricht, sobald sich die Daten während des Blätterns ändern. Wird ein Eintrag gelöscht, während jemand auf Seite drei ist, überspringt er einen. Wird einer eingefügt, sieht er einen doppelt. Bei großen Datenmengen kommt hinzu, dass die Datenbank für offset=100000 alle vorherigen Zeilen durchgehen muss.

Cursor (?nach=eyJpZCI6NDJ9&limit=20) übergibt eine Position statt einer Nummer. Der Server gibt in der Antwort den Cursor für die nächste Seite mit. Das ist stabil gegenüber Änderungen und bleibt auch bei Millionen Einträgen schnell.

Die Empfehlung: Cursor, sobald die Datenmenge wachsen kann oder sich häufig ändert. Offset nur für kleine, stabile Listen — dort ist es einfacher und völlig ausreichend.

Für Filter und Sortierung lohnt eine früh festgelegte Konvention, weil sie sonst je Endpunkt anders aussieht:

GET /rechnungen?status=offen&erstellt_ab=2026-01-01&sort=-erstellt&limit=20

Authentifizierung und Schlüssel

Drei Verfahren, drei Anwendungsfälle:

  • API-Schlüssel — einfach, für Server-zu-Server-Verbindungen mit einem festen Gegenüber.
  • Token (meist JWT) — für Sitzungen, mit Ablauf und Ansprüchen im Token.
  • OAuth — wenn ein Nutzer einer fremden Anwendung Zugriff auf seine Daten gewährt.

API-Schlüssel richtig erzeugen und speichern

Danach wird auffällig oft gesucht, und die Antwort ist kurz:

import secrets
schluessel = "sk_" + secrets.token_urlsafe(32)   # ~256 Bit Entropie

Drei Regeln dazu:

  • Kryptographisch sicherer Zufall, nicht der gewöhnliche Zufallsgenerator und keine Zeitstempel.
  • Als Hash speichern, nicht im Klartext. Ein Schlüssel ist ein Passwort. Wird die Datenbank kopiert, sind alle Schlüssel kompromittiert — es sei denn, dort stehen nur Hashes.
  • Ein Präfix voranstellen. sk_ oder ähnlich macht Schlüssel in Protokollen und Repositories erkennbar; Suchwerkzeuge finden versehentlich veröffentlichte Schlüssel daran.

Und was ein Schlüssel niemals tun sollte: in einem Abfrageparameter stehen. Der landet in Server- und Proxy-Protokollen.

Schnittstellen zwischen Diensten

Für interne Schnittstellen gelten andere Regeln als für öffentliche — das ist kein Nachlassen, sondern eine andere Kosten-Nutzen-Lage:

  • Man kennt alle Aufrufer. Eine brechende Änderung ist abstimmbar, wenn auch nicht kostenlos.
  • Ein API-Gateway ist der Ort für Authentifizierung, Ratenbegrenzung und die eine öffentliche Fassade über mehreren Diensten.
  • Vertragstests ersetzen die formale Versionierung teilweise: Der Aufrufer beschreibt seine Erwartung, der Anbieter prüft sie im eigenen Testlauf. Wie das zusammenspielt, steht in Microservices bauen.

Ein Sonderfall, der zeigt, wie sich Schnittstellenfragen wiederholen: Beim Model Context Protocol beschreibt ein Server seine Werkzeuge maschinenlesbar, damit ein Sprachmodell sie benutzen kann. Dieselben Fragen — Benennung, Parameter, Fehlerformat, Versionierung —, nur ist der Aufrufer diesmal kein Programm mit festem Code, sondern ein Modell, das aus der Beschreibung schließt. Umso wichtiger sind eindeutige Namen.

REST, GraphQL oder gRPC

REST GraphQL gRPC
Stärke einfach, überall unterstützt, cachebar Client bestimmt die Felder schnell, typisiert, Streaming
Schwäche Über- und Unterabruf von Daten Caching und Ratenbegrenzung schwierig Browser nur über Umwege
Gut für öffentliche Schnittstellen, CRUD Oberflächen mit vielen Sichten Dienst-zu-Dienst, hohe Frequenz
Schema OpenAPI (optional) eingebaut Protocol Buffers

Die praktische Empfehlung: REST für alles, was nach außen geht, weil jeder es versteht und die Werkzeuglandschaft vollständig ist. gRPC für Dienst-zu-Dienst-Verkehr mit hoher Frequenz. GraphQL, wenn viele verschiedene Oberflächen unterschiedliche Ausschnitte derselben Daten brauchen — und mit dem Wissen, dass Caching und Missbrauchsschutz dort eigene Arbeit sind.

Häufige Fragen

Was ist eine API kurz erklärt? Ein Vertrag zwischen zwei Systemen: Er sagt zu, welche Anfragen entgegengenommen werden und was zurückkommt.

Wie schneidet man Ressourcen? Nach Dingen, nicht nach Vorgängen. Substantive im Plural, keine Verben im Pfad, Hierarchie nur bis zur zweiten Ebene.

Welche Statuscodes wofür? 2xx für Erfolg, 4xx für Fehler des Aufrufers, 5xx für eigene Fehler. Der häufigste Fehler ist, alles mit 200 zu beantworten — damit funktioniert keine Zwischenschicht mehr.

Wie versioniert man eine Schnittstelle? Am pragmatischsten über den Pfad. Wichtiger als der Weg ist, additive Änderungen so zu gestalten, dass keine neue Version nötig wird.

Wie sieht ein gutes Fehlerformat aus? Einheitlich über die ganze Schnittstelle, mit stabilem Fehlercode, Korrelations-ID und ohne interne Details.

Offset oder Cursor für Paginierung? Cursor, sobald die Daten wachsen oder sich ändern. Offset überspringt und wiederholt Einträge, wenn sich die Liste während des Blätterns verändert.

Zusammenfassung und praktische Tipps

Schnittstellen sind Versprechen, und die teuren Fehler passieren am Anfang. Die wichtigsten Punkte:

  • Spezifikation zuerst. Ein Entwurf lässt sich in einer halben Stunde besprechen, eine fertige Schnittstelle nicht mehr ändern.
  • Substantive statt Verben, Hierarchie höchstens zwei Ebenen tief.
  • Statuscodes ernst nehmen. Alles mit 200 zu beantworten macht jede Zwischenschicht blind.
  • Ein Fehlerformat für alles, mit stabilem Code und Korrelations-ID.
  • Wisse, was brechend ist — besonders bei erweiterten Aufzählungen, dem am häufigsten übersehenen Fall.
  • Cursor statt Offset, sobald die Liste wächst.
  • Schlüssel als Hash speichern, mit Präfix und aus sicherem Zufall.

Mein Rat für den Einstieg: Schreib die Spezifikation für die drei wichtigsten Endpunkte, bevor du eine Zeile Code schreibst, und lass sie von jemandem gegenlesen, der sie später benutzen muss. Diese eine Stunde ist die günstigste Gelegenheit, die du je haben wirst, um eine Schnittstelle zu ändern — danach steigt der Preis mit jedem Tag.

Ressourcen