Breaking Changes schlecht! API Versionierung gut!
Wie jeder, der eine API entwickelt oder regelmäßig nutzt, früher oder später erkennt, sind breaking changes sehr schlecht und können ein ernsthaftes Manko für eine ansonsten nützliche API darstellen. Ein breaking change ist eine Änderung des Verhaltens einer API, die die Integration eines Nutzers brechen und zu viel Frustration sowie einem Verlust des Vertrauens zwischen dem API-Anbieter und dem Nutzer führen kann. Breaking changes erfordern, dass die Nutzer im Voraus benachrichtigt werden (mit begleitenden mea culpas), anstelle einer Änderung, die einfach auftritt, wie beispielsweise eine erfreuliche neue Funktion. Der Weg, diese Frustration zu vermeiden, besteht darin, eine API mit Versionen zu versehen, mit der Zusicherung des API-Eigentümers, dass innerhalb einer einzelnen Version keine überraschenden Änderungen eingeführt werden.
Wie schwer kann es also sein, eine API zu versionieren? Die Wahrheit ist, es ist nicht schwer, aber was schwierig ist, ist den Überblick zu behalten, ohne sich unnötig in eine schwindelerregende Anzahl von Versionen und Subversionen auf Dutzenden von API-Endpunkten mit unklaren Kompatibilitäten zu verlieren.
Wir haben v1 der API vor drei Jahren eingeführt und nicht erkannt, dass sie bis heute so gut funktioniert. Wie haben wir es also geschafft, die beste E-Mail-Zustellungs-API seit über zwei Jahren bereitzustellen und dennoch dieselbe API-Version beizubehalten? Während es viele verschiedene Meinungen darüber gibt, wie man REST-APIs versioniert, hoffe ich, dass die Geschichte unserer bescheidenen, aber leistungsstarken v1 Ihnen auf dem Weg zur API-Versionierungsaufklärung helfen kann.
REST ist am besten
Die SparkPost API stammt aus der Zeit, als wir Message Systems waren, bevor wir unsere Abenteuer in der Cloud starteten. Zu diesem Zeitpunkt waren wir beschäftigt, die letzten Vorbereitungen für den Beta-Start von Momentum 4 zu treffen. Dies war ein großes Upgrade auf Version 3.x, unser marktführendes On-Premise-MTA. Momentum 4 beinhaltete eine völlig neue UI, Echtzeitanalysen und vor allem eine neue Web-API für Nachrichteninjektion und -generierung, das Verwalten von Vorlagen und das Abrufen von E-Mail-Metriken. Unsere Vision war eine API-first-Architektur – bei der sogar die UI mit API-Endpunkten interagieren würde.
Eine der frühesten und besten Entscheidungen, die wir getroffen haben, war die Annahme eines RESTful-Stils. Seit den späten 2000er Jahren sind repräsentative Statusübertragungen (REST) basierte Web-APIs der De-facto-Standard für Cloud-APIs. Die Verwendung von HTTP und JSON erleichtert Entwicklern, unabhängig von der Programmiersprache, die sie verwenden – PHP, Ruby und Java – die Integration mit unserer API, ohne sich um die zugrunde liegende Technologie kümmern zu müssen.
Die Entscheidung, die RESTful-Architektur zu verwenden, war einfach. Eine Versionierungs-Konvention auszuwählen, war jedoch nicht so einfach. Ursprünglich haben wir die Frage der Versionierung aufgeschoben, indem wir die Beta gar nicht versionierten. Doch innerhalb von ein paar Monaten hatte die Beta einige Kunden erreicht, und wir begannen, unseren Cloud-Service auszubauen. Zeit für die Versionierung. Wir haben zwei Versionierungs-Konventionen evaluiert. Die erste war, die Versionierung direkt in der URI zu platzieren, und die zweite war, einen Accept-Header zu verwenden. Die erste Option ist expliziter und weniger kompliziert, was es einfacher für Entwickler macht. Da wir Entwickler lieben, war das die logische Wahl.
API-Governance
Nachdem wir eine Versionierungs-Konvention ausgewählt hatten, hatten wir weitere Fragen. Wann würden wir die Version erhöhen? Was ist ein breaking change? Würden wir die gesamte API oder nur bestimmte Endpunkte neu versionieren? Bei SparkPost haben wir mehrere Teams, die an verschiedenen Teilen unserer API arbeiten. Innerhalb dieser Teams arbeiten die Leute zu unterschiedlichen Zeiten an unterschiedlichen Endpunkten. Daher ist es sehr wichtig, dass unsere API in der Verwendung von Konventionen konsistent ist. Das war größer als die Versionierung.
Wir haben eine Governance-Gruppe gegründet, in der Ingenieure aus jedem Team, ein Mitglied des Produktmanagement-Teams und unser CTO vertreten sind. Diese Gruppe ist verantwortlich für die Etablierung, Dokumentation und Durchsetzung unserer API-Konventionen über alle Teams hinweg. Ein API-Governance Slack-Kanal ist ebenfalls nützlich für lebhafte Diskussionen zu diesem Thema.
Die Governance-Gruppe identifizierte eine Reihe von Möglichkeiten, wie Änderungen in die API eingeführt werden können, die für den Nutzer vorteilhaft sind und keinen breaking change darstellen. Dazu gehören:
Eine neue Ressource oder API-Endpunkt
Ein neuer optionaler Parameter
Eine Änderung an einem nicht-öffentlichen API-Endpunkt
Ein neuer optionaler Schlüssel im JSON-POST-Körper
Ein neuer Schlüssel, der im JSON-Antwortkörper zurückgegeben wird
Umgekehrt umfasste ein breaking change alles, was die Integration eines Nutzers brechen könnte, wie:
Ein neuer erforderlicher Parameter
Ein neuer erforderlicher Schlüssel in POST-Körpern
Entfernung eines bestehenden Endpunkts
Entfernung einer bestehenden Endpunkt-Anforderungsmethode
Ein wesentlich anderes internes Verhalten eines API-Aufrufs – wie eine Änderung des Standardverhaltens.
Die große 1.0
Als wir diese Konventionen dokumentierten und diskutierten, kamen wir auch zu dem Schluss, dass es im besten Interesse aller (einschließlich unseres!) lag, breaking changes an der API zu vermeiden, da die Verwaltung mehrerer Versionen erhebliche Zusatzaufwände mit sich bringt. Wir entschieden, dass es einige Dinge gab, die wir mit unserer API beheben sollten, bevor wir uns zu "v1" verpflichten.
Das Versenden einer einfachen E-Mail erforderte viel zu viel Aufwand. Um "die einfachen Dinge einfach zu halten", haben wir den POST-Körper aktualisiert, um sicherzustellen, dass sowohl einfache als auch komplexe Anwendungsfälle berücksichtigt werden. Das neue Format war auch zukunftssicherer. Zweitens haben wir ein Problem mit dem Metriken-Endpunkt angegangen. Dieser Endpunkt verwendete einen „group_by“-Parameter, der das Format des GET-Antwortkörpers so änderte, dass der erste Schlüssel der Wert des group-by-Parameters war. Das schien nicht sehr RESTful zu sein, also haben wir jede Gruppierung in einen separaten Endpunkt aufgeteilt. Schließlich haben wir jeden Endpunkt geprüft und kleinere Änderungen hier und da vorgenommen, um sicherzustellen, dass sie den Standards entsprechen.
Genauige Dokumentation
Es ist wichtig, eine genaue und benutzbare API-Dokumentation zu haben, um breaking changes, ob absichtlich oder unbeabsichtigt, zu vermeiden. Wir haben uns entschieden, einen einfachen Ansatz zur API-Dokumentation zu verwenden, der eine Markdown-Sprache namens API Blueprint verwendet und unsere Dokumente in Github verwaltet. Unsere Community trägt zu diesen Open-Source-Dokumenten bei und verbessert sie. Wir halten auch ein nicht-öffentliches Set von Dokumenten in Github für interne APIs und Endpunkte.
Ursprünglich haben wir unsere Dokumente an Apiary veröffentlicht, ein großartiges Tool zum Prototyping und Veröffentlichen von API-Dokumenten. Allerdings funktioniert das Einbetten von Apiary in unsere Website nicht auf mobilen Geräten, also verwenden wir nun Jekyll, um statische Dokumente zu generieren. Unsere neuesten SparkPost API-Dokumente laden jetzt schnell und funktionieren gut auf mobilen Geräten, was für Entwickler wichtig ist, die nicht immer an ihrem Computer sitzen.
Trennung von Bereitstellung und Veröffentlichung
Wir haben früh gelernt, dass es sinnvoll ist, eine Bereitstellung von einer Veröffentlichung zu trennen. Auf diese Weise ist es möglich, häufig Änderungen bereitzustellen, wenn sie bereit sind, durch kontinuierliche Lieferung und Bereitstellung, aber wir kündigen oder dokumentieren sie nicht immer öffentlich zur gleichen Zeit. Es ist nicht ungewöhnlich, dass wir einen neuen API-Endpunkt oder eine Verbesserung eines bestehenden API-Endpunkts bereitstellen und ihn von innerhalb der UI oder mit internen Tools nutzen, bevor wir ihn öffentlich dokumentieren und unterstützen. So können wir einige Anpassungen für die Benutzerfreundlichkeit oder die Einhaltung von Standards vornehmen, ohne uns um einen gefürchteten breaking change zu sorgen. Sobald wir mit der Änderung zufrieden sind, fügen wir sie unserer öffentlichen Dokumentation hinzu.
Doh!
Es ist nur fair zuzugeben, dass es Zeiten gegeben hat, in denen wir unseren Idealen "keine breaking changes" nicht gerecht geworden sind, und aus diesen Erfahrungen können wir lernen. Bei einer Gelegenheit haben wir beschlossen, dass es für die Nutzer besser wäre, wenn eine bestimmte Eigenschaft standardmäßig auf wahr anstatt auf falsch gesetzt werden würde. Nachdem wir die Änderung bereitgestellt hatten, erhielten wir mehrere Beschwerden von Nutzern, da sich das Verhalten unerwartet geändert hatte. Wir haben die Änderung rückgängig gemacht und eine kontobasierte Einstellung hinzugefügt – ein deutlich benutzerfreundlicherer Ansatz.
Gelegentlich sind wir versucht, breaking changes als Folge von Fehlerbehebungen einzuführen. Wir haben uns jedoch entschieden, diese Eigenheiten in Ruhe zu lassen, anstatt das Risiko einzugehen, die Integrationen der Kunden für den Preis der Konsistenz zu brechen.
Es gibt seltene Fälle, in denen wir die ernsthafte Entscheidung getroffen haben, einen breaking change vorzunehmen – wie das Deaktivieren einer API-Ressource oder -Methode – im Interesse der größeren Nutzer-Community und nur nachdem wir bestätigt haben, dass es eine geringe bis keine Auswirkung auf die Nutzer gibt. Zum Beispiel haben wir absichtlich die Wahl getroffen, das Antwortverhalten der Suppressions-API zu ändern, aber nur nachdem wir die Vorteile und Auswirkungen auf die Community sorgfältig abgewogen und die Veränderung unseren Nutzern klar kommuniziert hatten. Allerdings würden wir niemals eine Änderung einführen, die mit einer geringen Wahrscheinlichkeit die Zustellung einer Produktions-E-Mail eines Nutzers direkt beeinträchtigen könnte.