Entwicklung & Technik8 Min. Lesezeit02. Juni 2026
Engineering-Handwerk · Tag 3: API-Design, das nicht bricht
Warum APIs brechen und wie du sie so entwirfst, dass sie Änderungen überstehen: Konsistenz, Versionierung, Fehlerformat, Pagination und Idempotenz.
Willkommen zu Tag 3 unserer Serie Engineering-Handwerk. In Tag 2 ging es um Architektur, die mitwächst. Heute wird es unbequemer, denn heute geht es um etwas, das du nicht mehr allein zurücknehmen kannst, sobald es einmal draußen ist: die API. Sobald der erste fremde Client deinen Endpunkt aufruft, gehört die Schnittstelle nicht mehr dir. Sie gehört allen, die sich darauf verlassen. Und genau deshalb ist gutes API-Design keine Fleißaufgabe für später, sondern die eine Entscheidung, die dich noch in einem Jahr entweder ruhig schlafen oder um drei Uhr nachts einen Hotfix schreiben lässt.
Wir haben bei Forge12 genug APIs gebaut, geerbt und wieder geradegebogen, um eine unangenehme Wahrheit gelernt zu haben: Die meisten APIs brechen nicht, weil jemand einen groben Fehler gemacht hat. Sie brechen, weil viele kleine, gut gemeinte Entscheidungen sich über Monate zu einem Vertrag summieren, den niemand mehr sauber ändern kann. In diesem Artikel zeigen wir dir, worauf es beim API-Design wirklich ankommt, wo die typischen Bruchstellen liegen und wie du eine Schnittstelle baust, die Änderungen übersteht.
Warum APIs überhaupt brechen
Eine API ist ein Versprechen. Du sagst einem fremden System: „Wenn du mir das schickst, bekommst du das zurück." Jeder Client, jede Integration, jedes Mobile-App-Release baut auf diesem Versprechen auf. Das Problem: Software will sich ändern, Versprechen wollen bestehen bleiben. Aus diesem Spannungsfeld entsteht fast jeder API-Schmerz.
Interessant ist, dass die Brüche selten dort passieren, wo man sie erwartet. Ein neuer Endpunkt ist harmlos, den nutzt anfangs niemand. Gefährlich sind die stillen Änderungen an bestehenden Feldern: Ein Datumsformat, das plötzlich anders serialisiert wird. Ein Feld, das von String zu Objekt wechselt. Eine Fehlermeldung, deren Wortlaut ein Client versehentlich zum Parsen benutzt hat. Nichts davon wirft beim Deployment einen Fehler. Es funktioniert bei dir lokal, es geht durch die Tests, und drei Wochen später meldet ein Kunde, dass seine Buchhaltungs-Integration seit dem letzten Update keine Rechnungen mehr zieht.
Der Unterschied zwischen Breaking und Non-Breaking
Diese Unterscheidung solltest du im Schlaf beherrschen, denn sie entscheidet über die Hälfte deiner Design-Fragen. Ein Breaking Change ist alles, was einen bestehenden, korrekt gebauten Client kaputtmacht: ein Feld entfernen, einen Feldtyp ändern, ein Feld verpflichtend machen, das vorher optional war, einen Enum-Wert umbenennen, einen Endpunkt verschieben, das Verhalten bei gleicher Eingabe ändern.
Ein Non-Breaking Change ist dagegen additiv und rückwärtskompatibel: ein neues optionales Feld in der Antwort, ein neuer optionaler Query-Parameter, ein zusätzlicher Enum-Wert, den alte Clients ignorieren dürfen, ein völlig neuer Endpunkt. Die Faustregel, die wir intern benutzen: Nimm nie etwas weg und ändere nie die Bedeutung von etwas, das schon da ist. Hinzufügen darfst du fast immer. Diese eine Regel verhindert erstaunlich viele nächtliche Hotfixes.
Die sechs Bausteine, die gutes API-Design ausmachen
Wenn wir eine neue Schnittstelle entwerfen, prüfen wir sie gegen sechs Dinge. Keines davon ist spektakulär, und genau das ist der Punkt. Gute APIs sind langweilig, weil sie vorhersehbar sind.
Konsistenz schlägt Cleverness
Der wichtigste Wert einer API ist, dass sie sich überall gleich anfühlt. Wenn deine Liste von Nutzern unter einem Plural liegt, dann liegt auch die Liste von Rechnungen unter einem Plural. Wenn ein Zeitstempel an einer Stelle die Endung für den Erstellungszeitpunkt trägt, dann heißt er überall so. Ein Entwickler, der drei deiner Endpunkte kennt, sollte den vierten erraten können, ohne die Doku zu öffnen. Das klingt banal, ist aber der stärkste Effizienzhebel überhaupt: Konsistenz ist Dokumentation, die du nicht schreiben musst.
Der Gegner heißt Cleverness. Der eine Endpunkt, der aus historischen Gründen ein Array statt eines Objekts zurückgibt. Die eine Ressource, die im Singular liegt, weil es sich damals besser anfühlte. Jede Ausnahme kostet jeden Client-Entwickler ein Stück Vertrauen und dich langfristig eine Sonderbehandlung im Code.
Namen, die man nicht erklären muss
Ressourcen sind Substantive, keine Verben. Du fragst nicht nach einer Aktion, du fragst nach einem Ding und sagst über die HTTP-Methode, was damit passieren soll. Ein Feld, das einen Wahrheitswert trägt, sollte durch seinen Namen als solcher erkennbar sein. Ein Feld, das ein Datum trägt, ebenso. Wenn du ein Feld beim Namen nur mit einem Kommentar erklären kannst, ist der Name falsch. Namen sind das teuerste Element einer API, weil sie sich am schwersten ändern lassen: Sobald ein Feld einen Namen hat und genutzt wird, ist dieser Name für Jahre zementiert.
Versionierung, bevor du sie brauchst
Baue Versionierung von Anfang an ein, auch wenn du sie am ersten Tag nicht nutzt. Ein Präfix wie eine erste Version im Pfad kostet dich nichts und gibt dir später die Möglichkeit, eine zweite Generation parallel zu betreiben. Der Fehler, den wir immer wieder sehen: APIs ohne Versionierung, bei denen ein echter Breaking Change dann heißt, alle Clients gleichzeitig umzustellen. Das gelingt nie gleichzeitig. Wichtig ist die Haltung dahinter: Eine neue Version legst du nur bei echten Breaking Changes an, nicht für jedes neue Feld. Additive Änderungen bleiben in der bestehenden Version. Sonst hast du in einem Jahr fünf Versionen, die alle gepflegt werden wollen.
Ein Fehlerformat, das überall gleich aussieht
Fehler sind Teil deiner API, nicht ein Unfall am Rand. Ein Client muss programmatisch entscheiden können, was schiefgelaufen ist, ohne deine Fehlertexte zu parsen. Deshalb hat jeder Fehler bei uns dieselbe Struktur: eine lesbare Nachricht für Menschen, einen stabilen maschinenlesbaren Code für die Programmlogik und ein optionales Detailfeld für Validierungsfehler mit Feldbezug. Der maschinenlesbare Code ist entscheidend, denn er ist der einzige Teil des Fehlers, den ein Client zuverlässig auswerten darf. Die Nachricht darfst du jederzeit umformulieren, den Code niemals umbenennen. Sobald du dieses Format einmal festlegst und wirklich überall durchziehst, sparst du dir Sonderbehandlung in jedem einzelnen Client.
Pagination, die auch bei einer Million Einträgen hält
Jede Liste braucht von Tag eins eine Begrenzung. Ein Endpunkt, der heute fünf Einträge zurückgibt, gibt in zwei Jahren vielleicht fünfzigtausend zurück, und dann bringt die eine unbegrenzte Abfrage deine Datenbank und den Client ins Schwitzen. Wir setzen auf Cursor-basierte Pagination statt auf Seitenzahlen mit Offset. Der Grund ist praktisch: Offset-Pagination verschiebt sich, wenn zwischen zwei Seitenabrufen neue Datensätze eingefügt werden, und dann sieht ein Client Einträge doppelt oder überspringt sie. Ein Cursor zeigt stabil auf eine Position und übersteht Einfügungen. Ein sinnvoller Standard für die Seitengröße und eine harte Obergrenze gehören zwingend dazu, sonst umgeht jemand deine Begrenzung mit einer riesigen Limit-Angabe.
Idempotenz bei Schreib-Operationen
Das ist der Baustein, der am häufigsten fehlt und am teuersten wird. Netzwerke sind unzuverlässig. Ein Client schickt eine Anfrage, bekommt aber keine Antwort, weil die Verbindung abbrach. Hat der Server die Aktion ausgeführt oder nicht? Der Client weiß es nicht und versucht es erneut. Wenn dein Endpunkt nicht idempotent ist, hast du jetzt zwei Rechnungen, zwei Buchungen, zwei Abbuchungen. Lesende Abfragen sind von Natur aus idempotent, das Ersetzen einer Ressource ebenfalls. Gefährlich ist das reine Erzeugen. Hier hilft ein Idempotenz-Schlüssel: Der Client schickt einen selbst erzeugten eindeutigen Schlüssel mit, der Server merkt sich, dass er diese Anfrage schon verarbeitet hat, und gibt beim zweiten Versuch dasselbe Ergebnis zurück, ohne die Aktion noch einmal auszuführen. Für alles, was Geld bewegt oder Zustand erzeugt, ist das keine Kür, sondern Pflicht.
Ein Praxisbeispiel aus unserem Alltag
Ein konkreter Fall, weil er alle Themen zusammenbringt. Wir hatten einen Endpunkt, der Bestellungen erzeugt. In der ersten Version sah die Antwort so aus: ein Statusfeld, das einen einzelnen Text enthielt, etwa den Wert für „bezahlt". Ein Client, eine Buchhaltungs-Integration eines Kunden, hat auf diesen exakten Text geprüft, um zu entscheiden, ob eine Rechnung erstellt werden soll.
Nach einigen Monaten brauchten wir mehr Zwischenzustände, weil Zahlungen jetzt über einen externen Anbieter liefen und es einen Schwebezustand zwischen „offen" und „bezahlt" gab. Der naheliegende, falsche Weg wäre gewesen, das bestehende Statusfeld umzudeuten oder in ein Objekt mit Unterfeldern zu verwandeln. Beides hätte die Buchhaltungs-Integration des Kunden ohne Vorwarnung gebrochen, weil sein Code auf den alten String traf und ihn nicht mehr fand.
Wir haben stattdessen additiv gearbeitet. Das alte Statusfeld blieb unverändert und lieferte weiterhin genau die Werte, die es immer geliefert hatte. Der neue Schwebezustand wurde in einem zusätzlichen, optionalen Feld für den Zahlungsstatus abgebildet, das alte Clients schlicht ignorieren. Wer den feineren Zustand wollte, las das neue Feld; wer nur „bezahlt oder nicht" brauchte, blieb beim alten. Keine neue Version nötig, keine Absprache mit Kunden, kein Hotfix. Der Endpunkt zum Erzeugen bekam obendrein einen Idempotenz-Schlüssel, weil genau hier ein Retry nach Timeout die teuerste aller Doppelbuchungen ausgelöst hätte.
Die Lehre daraus ist unbequem: Der elegante Weg, das Statusfeld sauber zu einem Objekt umzubauen, wäre technisch schöner gewesen. Aber Schönheit ist bei einer öffentlichen API zweitrangig gegenüber Stabilität. Der Kunde interessiert sich nicht für dein Datenmodell, er interessiert sich dafür, dass seine Rechnungen weiter erstellt werden.
So gehst du vor
Wenn du eine neue API oder einen neuen Endpunkt entwirfst, empfehlen wir diese Reihenfolge. Sie ist bewusst design-first und nicht code-first, weil sich der teuerste Fehler, ein schlechter Vertrag, am billigsten auf dem Papier korrigieren lässt.
- Schreibe zuerst den Vertrag, nicht den Code. Skizziere Endpunkte, Ressourcennamen, Felder und Fehlercodes, bevor du eine Zeile Implementierung schreibst. Lies es laut. Wenn du einen Namen erklären musst, ändere ihn jetzt, solange es nichts kostet.
- Prüfe jede Antwort gegen die Wachstumsfrage. Was passiert mit diesem Feld, wenn es davon eine Million gibt? Was, wenn dieser eine Wert später drei Varianten hat? Wenn die Antwort ein Breaking Change wäre, baue heute schon Spielraum ein, etwa durch ein Objekt statt eines nackten Werts, wo du echte Erweiterung erwartest.
- Lege das Fehlerformat einmal fest und ziehe es kompromisslos durch. Ein einziger Endpunkt mit abweichender Fehlerstruktur reißt das ganze Prinzip ein. Konsistenz gibt es nur ganz oder gar nicht.
- Markiere jede schreibende Operation als sicher oder riskant. Alles, was Zustand erzeugt oder Geld bewegt, bekommt Idempotenz. Kläre das im Design, nicht im Incident.
- Dokumentiere die Änderung, nicht nur den Endpunkt. Halte fest, was neu ist, was sich ändert und ob es ein Breaking Change ist. Ein sauberer Changelog ist Teil des API-Vertrags, kein Beiwerk.
- Behandle die Doku als Teil des Deployments. Eine API, deren Verhalten nicht mehr zur Beschreibung passt, ist gefährlicher als eine undokumentierte, weil sie aktiv in die Irre führt. Aktualisiere beides im selben Schritt.
Und ein Wort zur Doku als Vertrag: Die verlässlichste Dokumentation ist die, die aus der Schnittstelle selbst entsteht, etwa über ein maschinenlesbares Schema, das direkt aus dem Code erzeugt wird. Handgepflegte Doku driftet, und driftende Doku ist schlimmer als keine, weil ihr jemand glaubt. Wenn Beschreibung und Verhalten aus derselben Quelle kommen, können sie gar nicht erst auseinanderlaufen.
Die typischen Fehler, die APIs später brechen
Zum Mitnehmen die Bruchstellen, die uns am häufigsten begegnen. Erstens: das Umdeuten bestehender Felder, weil ein neuer Fall reingedrückt werden muss. Zweitens: fehlende Pagination, die erst bei echtem Datenwachstum auffällt und dann nur noch als Breaking Change nachrüstbar ist. Drittens: Fehlermeldungen ohne stabilen Code, sodass Clients auf Freitext parsen. Viertens: nicht-idempotente Erzeugen-Endpunkte, die bei jedem Timeout Duplikate produzieren. Fünftens: keine Versionierung, sodass jede echte Änderung zur koordinierten Massenmigration wird. Alle fünf haben eines gemeinsam: Sie kosten beim Bauen fast nichts und beim Reparieren fast alles.
Fazit
Gutes API-Design ist kein Akt der Genialität, sondern der Disziplin. Konsistenz statt Cleverness, additive Änderungen statt Umdeutungen, stabile Fehlercodes, Pagination und Idempotenz von Anfang an, und eine Doku, die mit dem Code lebt. Keine dieser Regeln ist schwer zu verstehen. Schwer ist nur, sie auch dann durchzuhalten, wenn die schnelle Ausnahme verlockt. Genau diese Disziplin entscheidet, ob deine Schnittstelle in sechs Monaten noch trägt oder ob du sie an einem Freitagabend notdürftig flickst. Baue den Vertrag so, dass du ihn morgen noch einhalten willst.
Das war Tag 3. Die gesamte Reise findest du in der Serien-Übersicht. Wenn du eine API oder ein Backend baust, das nicht in sechs Monaten bricht, sondern mitwächst, dann sieh dir unsere Softwareentwicklung an oder nimm direkt Kontakt mit uns auf. Wir helfen dir, Schnittstellen zu entwerfen, die stabil bleiben, während dein Produkt sich verändert.