Alles, was ich über gutes API-Design weiß
(seangoedecke.com)- In der Softwareentwicklung sind APIs ein zentrales Werkzeug, und eine gute API sollte so vertraut und einfach sein, dass sie schon fast langweilig wirkt
- Da sich eine API nach ihrer Veröffentlichung nur schwer ändern lässt, ist das Prinzip die Umgebung der Nutzer nicht zu zerstören (WE DO NOT BREAK USERSPACE) von zentraler Bedeutung
- Wenn Änderungen unvermeidbar sind, ist Versionierung (versioning) nötig, aber sie ist ein notwendiges Übel, das Komplexität und Wartungskosten stark erhöht
- Die Qualität einer API hängt letztlich vom Wert des Produkts selbst ab; ein schlecht konzipiertes Produkt macht es schwer, eine gute API zu bauen
- Für Stabilität und Skalierbarkeit sollten API-Key-basierte Authentifizierung, Idempotenz, Rate Limits und cursorbasierte Pagination berücksichtigt werden
Einleitung: Bedeutung und Kontext von API-Design
- Eine der wichtigsten Aufgaben moderner Softwareingenieure ist die Interaktion mit APIs
- Der Autor hat selbst Erfahrung im Entwurf, in der Implementierung und in der Nutzung verschiedenster öffentlicher und interner APIs gesammelt, darunter REST, GraphQL und Kommandozeilen-Tools
- Bestehende Ratschläge zu API-Design neigen oft dazu, sich an komplexen Konzepten festzubeißen (Definitionen von REST, HATEOAS usw.)
- Dieser Text fasst auf Basis praktischer Erfahrung pragmatische Prinzipien für API-Design zusammen
Das Gleichgewicht zwischen Vertrautheit und Flexibilität: die erste Voraussetzung für eine gute API
- Eine gute API ist eine „gewöhnliche und langweilige“ API, also eine, deren Nutzung bestehenden APIs ähnelt, die man bereits kennt
- Nutzer wollen sich nicht auf die API selbst, sondern auf das Erreichen ihres Ziels konzentrieren, daher braucht es ein Design mit niedriger Einstiegshürde
- Eine einmal veröffentlichte API lässt sich nur sehr schwer ändern, weshalb schon in der ersten Entwurfsphase große Sorgfalt nötig ist
- Entwickler wünschen sich eine möglichst schlanke API, müssen aber zugleich immer mitdenken, wie sie langfristige Flexibilität erhalten
- Entscheidend ist daher letztlich die Balance zwischen Vertrautheit und langfristiger Flexibilität
User Space niemals kaputtmachen (WE DO NOT BREAK USERSPACE)
- Änderungen wie das Hinzufügen neuer Felder zu einer bestehenden Antwortstruktur sind in den meisten Fällen unproblematisch
- Das Entfernen von Feldern oder das Ändern von Typen bzw. Strukturen führt jedoch dazu, dass der Code aller Konsumenten bricht
- API-Verantwortliche haben die Verantwortung, die Software bestehender Nutzer nicht absichtlich unbrauchbar zu machen
- Selbst der Tippfehler im HTTP-Header „referer“ wird nicht korrigiert, weil es eine Kultur gibt, User Space zu bewahren
APIs ändern, ohne sie zu brechen: Strategien zur Versionierung
- Breaking Changes sollten nur dann zugelassen werden, wenn sie absolut unvermeidbar sind; dann ist Versionierung die richtige Antwort
- Alte und neue Versionen sollten parallel betrieben werden, um einen schrittweisen Übergang zu ermöglichen
- Versionskennungen können auf verschiedene Weise eingebunden werden, etwa über die URL (
/v1/) oder Header, sodass Nutzer in ihrem eigenen Tempo migrieren können - Versionierung bringt erhebliche Nachteile mit sich: enorme Wartungskosten (mehr Endpunkte, Tests, Support) und Verwirrung bei Nutzern
- Selbst wenn man wie Stripe eine interne Übersetzungsschicht einzieht, lässt sich die grundlegende Komplexität nicht vermeiden
- Die Einführung von API-Versionierung sollte das letzte Mittel sein
Der Erfolg einer API hängt vollständig vom Wert des Produkts ab
- Eine API ist im Kern nichts anderes als die Schnittstelle zu einem realen Geschäftsprodukt
- Bei APIs wie OpenAI oder Twilio wollten Nutzer letztlich genau die Funktionalität, die diese APIs bereitstellen
- Ein wertvolles Produkt wird auch dann genutzt, wenn seine API unbequem ist
- API-Qualität ist eher eine Eigenschaft am Rand: Sie wird vor allem dann zum Entscheidungskriterium, wenn die eigentliche Wettbewerbsfähigkeit ähnlich ist
- Umgekehrt ist ein Produkt ohne API für technische Nutzer ein großes Hindernis
Wenn das Produktdesign schlecht ist, kann auch die API nicht gut werden
- Selbst eine technisch sehr ausgereifte API ist bedeutungslos, wenn das Produkt keinen Marktwert hat
- Noch wichtiger: Wenn die grundlegende Ressourcenstruktur unlogisch oder ineffizient ist, zeigt sich das auch in der API
- Ein System, das Kommentare etwa als Linked List speichert, erschwert schon von Grund auf ein natürliches RESTful Design
- Technische Probleme, die sich in einer UI verstecken lassen, werden in einer API vollständig sichtbar und zwingen Nutzern unnötig ein tieferes Systemverständnis auf
Authentifizierung (Authenticaton) und die Vielfalt der Nutzer
- API-Key-basierte Authentifizierung mit langer Lebensdauer sollte unbedingt unterstützt werden
- Auch wenn zusätzlich sicherere Verfahren wie OAuth unterstützt werden, ist die Einstiegshürde bei API-Keys deutlich niedriger
- Zu den API-Konsumenten gehören nicht nur Ingenieure, sondern auch Nicht-Entwickler wie Vertrieb, Planung, Studierende oder Hobbyentwickler
- Schwierige oder komplexe Authentifizierungsanforderungen (wie OAuth) sind für nicht spezialisierte Nutzer eine Hürde
Idempotenz und Retry-Verarbeitung
- Bei aktionsbezogenen Requests (z. B. Zahlungen oder Statusänderungen) ist die Sicherheit bei Retries im Fehlerfall entscheidend
- Idempotenz bedeutet, dass dieselbe Anfrage mehrfach gesendet werden kann, das Ergebnis aber nur einmal verarbeitet wird
- Der Standardansatz ist, einen Idempotency Key als Parameter oder Header mitzugeben, um doppelte Verarbeitung zu verhindern
- Zum Speichern von Idempotency Keys reicht ein einfacher Key-Value-Store wie Redis aus; in den meisten Fällen ist auch ein periodisches Verfallenlassen unproblematisch
- Für Lese- oder Löschanfragen (im REST-Stil) ist das in der Regel nicht nötig
API-Sicherheit und Rate Limiting
- API-Anfragen per Code können in viel höherer Geschwindigkeit auftreten als durch manuelle Benutzerinteraktion
- Eine unbedacht veröffentlichte API kann auf unerwartete Weise genutzt werden, etwa für ein groß angelegtes Chat-System
- Rate Limiting ist zwingend erforderlich und sollte bei rechenintensiven Operationen strenger ausfallen
- Auch eine temporäre Deaktivierung der API für bestimmte Kunden (Killswitch) sollte als Option erwogen werden
- Über Response-Header wie
X-Limit-RemainingundRetry-Aftersollten Informationen zum Rate Limit kommuniziert werden
Pagination-Strategien
- Um große Datensätze (z. B. Millionen von Tickets) effizient zurückzugeben, ist Pagination unverzichtbar
- Offset-basierte Pagination ist einfach, wird bei großen Datenmengen jedoch zunehmend langsamer
- Cursorbasierte Pagination funktioniert auch bei sehr großen Datensätzen effektiv, ohne dass die Query-Performance einbricht
- Sie ist in Implementierung und Nutzung etwas schwieriger, dürfte langfristig aber eine unvermeidliche Entwicklung sein
- Es ist sinnvoll, in die Antwort ein Feld wie
next_pageaufzunehmen, um den Cursor für die nächste Anfrage klar anzugeben
Optionale Felder und die Sicht auf GraphQL
- Teure oder langsame Felder sollten in der Standardantwort weggelassen und nur bei Bedarf selektiv ergänzt werden
- Mit Parametern wie
includeskönnen verknüpfte Daten eingebunden werden - GraphQL bietet Vorteile bei der Flexibilität der Datenstruktur, bringt aber auch Probleme mit sich, etwa geringere Zugänglichkeit für Nicht-Entwickler, komplizierteres Caching und mehr Edge Cases sowie höhere Komplexität im Backend
- Nach praktischer Erfahrung sollte GraphQL nur dann eingeführt werden, wenn es wirklich nötig ist
Besonderheiten interner APIs
- Interne APIs unterscheiden sich in vielerlei Hinsicht von externen bzw. öffentlich bereitgestellten APIs
- Da die Konsumenten meist professionelle Softwareingenieure sind, sind komplexere Authentifizierung oder auch Breaking Changes eher möglich
- Dennoch bleiben Designprinzipien wie Idempotenz, Unfallvermeidung und die Minimierung des Betriebsaufwands gültig
Zusammenfassung
- APIs lassen sich nur schwer ändern und sollten leicht zu benutzen sein
- User Space nicht kaputtzumachen ist die wichtigste Pflicht von API-Verantwortlichen
- API-Versionierung sollte wegen ihrer hohen Kosten nur als letztes Mittel eingesetzt werden
- Letztlich wird die Qualität einer API vom eigentlichen Wert des Produkts bestimmt
- Ein schlecht entworfenes Produkt lässt sich auf API-Ebene nur begrenzt kompensieren
- Wichtig sind die Unterstützung einfacher Authentifizierung, für kritische aktionsbezogene Requests unbedingt Idempotenz, sowie Stabilitätsmaßnahmen wie Rate Limiting und Pagination
- Interne APIs erfordern je nach Zweck und Zielgruppe andere Strategien, dennoch bleibt sorgfältiges Design unverzichtbar
- Formate wie REST, JSON oder OpenAPI sind nicht der Kernpunkt. Wichtiger ist eine klare Dokumentation
1 Kommentare
Hacker-News-Kommentare
Der Rat „userspace niemals kaputtmachen“ ist berühmt, aber hier wird zu Recht auch die andere Seite erwähnt: „Kernel-APIs dürfen ohne Vorwarnung kaputtgehen“. Entscheidend ist nicht „man darf niemals irgendeine API brechen“, sondern das feinere Gleichgewicht: „Nur das, was ausdrücklich als stabil deklariert wurde, darf niemals gebrochen werden“
Selbst wenn der Linux-Kernel userspace nicht kaputtmacht, bricht GNU libc die userspace-Kompatibilität ziemlich häufig. Deshalb geht im Linux-Userspace trotz aller Bemühungen der Kernel-Entwickler regelmäßig etwas kaputt. Programme und Bibliotheken, die mit einer neuen libc-Version gebaut wurden, laufen auf älteren libc-Versionen mitunter nicht korrekt, sodass man am Ende alle Komponenten auf einmal upgraden muss. Ironischerweise hat Windows dieses Problem mit dem Redistributable-Ansatz schon vor Jahrzehnten gelöst
Unter Linux gibt es bekanntlich keine stabile öffentliche Treiber-API, und ich habe gehört, genau das sei ein Motiv für Google gewesen, Fuschia OS zu entwickeln. Linux verfolgt damit gegenüber Userspace und Hardware jeweils eine unterschiedlich ausgerichtete Linie
Der Autor scheint versionsbasierte APIs nicht besonders zu mögen, aber ich empfehle immer, Versionsverwaltung von Anfang an einzuführen, wenn man eine App baut. Die Zukunft lässt sich nicht vorhersagen, daher wird es irgendwann zwangsläufig auch bei einem selbst durch äußere Faktoren zu inkompatiblen Änderungen kommen
Eigentlich hat der Autor Versionsverwaltung meiner Meinung nach ebenfalls empfohlen. Im Text steht schließlich: „Versionen sind ein Weg, APIs verantwortungsvoll zu ändern.“ Insofern wird Versionsverwaltung letztlich durchaus befürwortet. Nur soll der Wechsel auf eine neue Version das letzte Mittel sein
Ich stimme der Ansicht zu, nicht einfach „v1“ an Endpoints zu hängen. In der Praxis wächst eine API so, dass man zunächst Felder oder Optionen an bestehende Endpoints ergänzt, um Abwärtskompatibilität zu wahren. Wenn dann etwas wirklich Inkompatibles nötig wird, vergibt man dem Endpoint meist gleich einen neuen Namen und erstellt einen komplett neuen Endpoint (statt einfach /v2). Falls die gesamte API umgebaut werden muss, stellt man den bestehenden Dienst oft ein und startet einen völlig neuen Service mit neuem Namen. In 25 Berufsjahren habe ich genau einmal einen Service gesehen, bei dem „/v1“ und „/v2“ parallel verwendet wurden
Ich glaube nicht, dass die Absicht des Autors war, /v1 grundsätzlich nicht in Endpoints aufzunehmen. Der Punkt ist vielmehr, alles daranzusetzen, dass es kein neues /v2 geben muss. Sobald /v2 existiert, müssen bei jedem Bugfix Änderungen in beiden Varianten vorgenommen werden, und die bedingten Verzweigungen wachsen exponentiell, bis die Codebasis spaghettiartig unübersichtlich wird. Letztlich war dann schon das ursprüngliche /v1-Design zu wenig auf Zukunftskompatibilität ausgelegt
Ich finde auch, dass man Versionierung problemlos später hinzufügen kann. Man kann zum Beispiel mit /api/posts beginnen und die nächste Version dann als /api/v2/posts ergänzen
Ich bin nicht dafür, Versionsnummern von Anfang an fest einzubauen. Das führt dazu, dass mehrere Versionen tatsächlich häufiger parallel genutzt werden, und ich halte das eher für schlechter
Dieser Artikel war sehr nützlich. Ich würde noch einen Rat ergänzen: Je schwerer API-Dokumentation zu bekommen ist, desto schlechter ist in der Regel die API. Wenn man erst einen Vertrag unterschreiben muss, um an die Dokumentation zu kommen, kann man ziemlich sicher davon ausgehen, dass die API mies ist
Es wurde empfohlen, den idempotency key nicht separat in einer Comment-Tabelle zu speichern, sondern in einem Key/Value-Store wie Redis. Ich frage mich aber, ob dieser Ansatz in allen Fehlerfällen wirklich verlässliche Idempotenz garantieren kann. Wenn der Server zum Beispiel bei einem bedingten Schreibvorgang wie
SET key 1 NXfeststellt, dass der Key bereits existiert, müsste er das Erstellen des Kommentars einfach überspringen. Zu diesem Zeitpunkt könnte die vorherige Anfrage aber in der DB noch gar nicht tatsächlich persistiert worden sein. Der idempotency key muss zusammen mit der eigentlichen Operation in derselben Transaktion committed und bei Bedarf auch zurückgerollt werden. Im Kern sollte ein idempotency key also die „eindeutige ID dieser Operation oder Anfrage“ sein. Zum Beispiel ein ressourcenspezifischer Bezeichner passend zu „Kommentar erstellen“, „Kommentar aktualisieren“ usw.Der Vorteil cursorbasierter Paginierung ist, dass Nutzer keine bereits gesehenen Einträge noch einmal sehen müssen, selbst wenn zwischen dem Laden einer Seite und dem Klick auf „Weiter“ neue Elemente hinzugekommen sind. Beim Cursor-Verfahren merkt man sich die Objekt-ID des letzten Elements der vorherigen Seite und liefert danach die folgenden Einträge aus, was besonders für Infinite Scroll nützlich ist. Der Nachteil ist, dass sich damit eine Funktion wie „zur N-ten Seite springen“ nur schwer umsetzen lässt
Wenn man heute „API“ sagt, denken die meisten an Requests an eine Web-App, bei denen man Parameter und Header setzt, um Daten abzurufen. Ursprünglich bedeutet API aber „Application Programming Interface“, also die Schnittstelle eines Anwendungsprogramms. Der Begriff wurde erstmals in den 1940er Jahren verwendet und hatte bis in die 1990er Jahre hinein fast keine andere Bedeutung. APIs haben also eine über 80-jährige Geschichte, und es gibt enorm viel altes Material dazu. Wenn man sich anschaut, mit welchen Problemen Programmierer damals zu tun hatten und wie sie sie gelöst haben, findet man wahrscheinlich auch heute noch Nützliches für sich selbst
Ich stimme nicht der Ansicht zu, interne Nutzer einfach nur als „Nutzer“ zu betrachten. Auch wenn sie technisch versierter sind und oft selbst Programmierer, sind sie ebenfalls beschäftigt und auf ihre eigenen Projekte fokussiert, sodass ihnen häufig Zeit und Spielraum fehlen, auf API-Änderungen zu reagieren. Wenn möglich, sollte man vor der Veröffentlichung innerhalb des Teams ausreichend „dogfooding“ betreiben. Sobald etwas extern veröffentlicht ist, muss das Versprechen „userspace nicht kaputtmachen“ unbedingt eingehalten werden
Für interne Nutzer sind meist Instrumentierungswerkzeuge implementiert, mit denen man sie direkt kontaktieren und zur Migration bewegen kann. Dadurch wird auch das Auslaufenlassen von API-Versionen möglich, weshalb eine strategische Einführung von Versionierung durchaus attraktiv ist. Ich habe selbst an echter API-Versionierung mitgewirkt und dabei im Vergleich zu Organisationen, die das grundsätzlich nicht nutzen, klar positive Effekte gesehen
Ich denke, Versionierung kann bei der Lösung dieses Problems helfen. Eine der besten Arten, interne Nutzer zu berücksichtigen, ist die gemeinsame Arbeit an der Spezifikation und das Teilen auch der in Arbeit befindlichen Version dieser Spezifikation mit den Stakeholdern. Selbst wenn es sich um fortlaufend aktualisierte Dokumentation handelt, schafft ein Referenzpunkt bessere Voraussetzungen für internes wie externes Feedback und ist sehr nützlich, solange man unnötige politische Reibungen vermeidet
Statt den idempotency key in Redis zu speichern, sollte man ihn nach Möglichkeit zusammen mit den eigentlichen Daten innerhalb derselben Transaktion speichern, in der diese geschrieben werden
Die Warnung „userspace niemals kaputtmachen“ ist wirklich wichtig. Es war schade zu sehen, wie Spotify, Reddit und Twitter dieses Prinzip in jüngerer Zeit ignoriert haben
Zur Referenz: Unter https://jcs.org/2023/07/12/api sind gute API-Empfehlungen übersichtlich zusammengefasst, daher lohnt sich ein Blick darauf