Was macht API-Versionierungsstrategie?
TL;DR: Datumsbasierte, pro API-Key gepinnte Versionen (Stripe-Modell) schlagen
/v1/,/v2/für langlebige öffentliche APIs, weil sie kleine breaking Changes erlauben, ohne Clients auf einen neuen Baum zu zwingen. Für interne APIs reichen additive Evolution plus dieDeprecation- undSunset-Header (RFC 9745, RFC 8594) meistens aus. Immer ankündigen, immer an einem Datum auslaufen lassen, niemals einfach entfernen.
Schnellnavigation nach Problem
| Symptom | Abschnitt |
|---|---|
| "Muss ein Feld brechen, habe aber 100.000 API-Keys" | Datumsbasierte Pinned-Versionen |
"Soll ich /v1/ oder Accept: application/vnd.foo.v2+json verwenden?" | Strategievergleich |
| "Wie sage ich Clients, dass ein Endpunkt verschwindet?" | Deprecation + Sunset |
| "Sechs Versionen im Code zu pflegen bringt uns um" | Version-Transformer |
| "Interne API – brauchen wir überhaupt Versionierung?" | Interne vs. öffentliche APIs |
Entscheidungsdiagramm
flowchart TD
A[Muss API-Verhalten ändern] --> B{Nur additiv?<br/>neues Feld, neuer Endpunkt, neuer optionaler Parameter}
B -->|Ja| C[Einfach ausrollen<br/>keine Versionierung nötig]
B -->|Nein, breaking| D{API ist öffentlich<br/>+ viele undurchsichtige Clients?}
D -->|Nein, intern/wenige Clients| E[Migration koordinieren<br/>+ Deprecation/Sunset-Header]
D -->|Ja, öffentlich| F{Kannst du pro Anfrage<br/>einen Kompatibilitätslayer bereitstellen?}
F -->|Ja, habe einen Transformer| G[Datumsbasierte Pinned-Version<br/>Stripe-Modell]
F -->|Nein, großer Rewrite| H[Major-Version URI-Bump<br/>/v1 → /v2]
E --> I[Setze Deprecation: @timestamp<br/>+ Sunset: HTTP-date<br/>+ Link: rel=deprecation]
G --> I
H --> I
1. Datumsbasierte Pinned-Versionen (Stripe-Modell)
Stripe hat seine Architektur in API-Versionierung bei Stripe veröffentlicht:
Wenn ein Benutzer zum ersten Mal eine API-Anfrage stellt, wird sein Konto automatisch auf die aktuellste verfügbare Version fixiert, und von da an wird jeder API-Aufruf implizit dieser Version zugeordnet.
[Versionen sind] rollierende Versionen, die nach dem Datum ihrer Veröffentlichung benannt sind (z.B.
2017-05-24).
Die Schlüsseleigenschaften:
| Eigenschaft | Warum es funktioniert |
|---|---|
| Standardversion pro Konto (beim ersten Aufruf gesetzt) | Neue Kunden werden automatisch auf die neueste Version fixiert; bestehende Kunden werden nicht unterbrochen, wenn du eine Änderung ausrollst |
Stripe-Version-Header überschreibt die Pinning pro Anfrage | Ermöglicht schrittweise Client-Migration: SDK kann sich für eine neue Version entscheiden, bevor das Konto es tut |
Datumsbasierte Namen (2024-04-10) | Vermittelt Aktualität; keine Debatte darüber, was "v3" bedeutet; erlaubt beliebig viele kleine Brüche, anstatt sie für einen großen Knall v3 aufzusparen |
| Dashboard-Upgrade-Pfad | Kunde kann den Unterschied einsehen und dann die Standardversion seines Kontos aktualisieren |
Dies ist der einzige Ansatz, der für wirklich langlebige öffentliche APIs skaliert (Stripe hat Versionen, die ein Jahrzehnt zurückreichen). Es kostet dich interne Komplexität (das Transformer-Pattern in §4), erspart deinen Kunden aber den ewigen /v1/ → /v2/-Migrationszyklus.
2. Versionierungsstrategie-Vergleich
| Strategie | Wo die Version lebt | Pro | Contra | Am besten geeignet für |
|---|---|---|---|---|
URI-Segment (/v1/, /v2/) | Pfad | Cache-freundlich (verschiedene URL = verschiedener Cache-Eintrag); keine Client-Konfiguration | Keine kleinen Brüche möglich; erzwingt eine Baumgabelung; URLs sind keine "stabilen Ressourcen-Identifikatoren" mehr (laut Fielding) | Interne APIs, öffentliche APIs, die selten brechen |
Benutzerdefinierter Header (Stripe-Version: 2024-04-10) | Anforderungsheader | Viele Versionen günstig; Granularität pro Anfrage | URLs können nicht mit eingebauter Version geteilt werden; schwerer im Browser-Adressfeld zu testen | Öffentliche APIs in großem Maßstab |
Accept-Medientyp (Accept: application/vnd.foo.v2+json) | Standard Accept-Header | "Spezifikationskonform" laut HTTP; nutzt die Content-Negotiation-Mechanismen | Umständlich zu setzen; Tooling-Unterstützung variiert; Debuggen mit curl ist ausführlich | Hypermedia-APIs, tiefgehend RESTful-Designs |
Abfrageparameter (?version=2) | URL | Einfach zu testen; in Logs sichtbar | Verschmutzt URL; semantisch falsch (Version ist keine Ressourceneigenschaft) | Nur schnelles Prototyping |
| Keine Versionierung, nur additiv | n/a | Kein Overhead; keine Client-Koordination | Nie eine breaking Change ohne neuen Endpunkt möglich | Interne Microservices mit gemeinsamem Deploy |
Wähle, indem du deine Einschränkung auswählst:
- "Wir machen häufig winzige Brüche" → datumsbasierter Header (Stripe)
- "Wir machen alle 5 Jahre einen großen Rewrite" → URI-Segment (
/v1/,/v2/) - "Wir brechen nie, fügen nur hinzu" → keine Versionierung + additive Evolution
3. Das Deprecation/Sunset-Header-Paar
Zwei IETF-RFCs decken das Lebenszyklus-Signal ab:
Sunset-Header — RFC 8594
Der Sunset-Wert ist ein HTTP-Datum-Zeitstempel, wie in Abschnitt 7.1.1.1 von [RFC7231] definiert, und SOLLTE ein Zeitstempel in der Zukunft sein.
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Zeigt an, dass "die Ressource voraussichtlich zu einem bestimmten Zeitpunkt in der Zukunft nicht mehr erreichbar sein wird." Clients SOLLTEN den Zeitstempel als Hinweis behandeln, nicht als festen Vertrag.
Deprecation-Header — RFC 9745
Deprecation: @1735689599
Sunset: Sun, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrate-v1-to-v2>; rel="deprecation"
Der Deprecation-Wert ist ein Unix-Zeitstempel (Sekunden, mit @-Präfix gemäß Structured Fields). Er kann in der Vergangenheit ("bereits veraltet") oder Zukunft ("wird veraltet sein") liegen. Die MUSS-Regeln:
- MUSS das Structured-Field-Datumsformat gemäß RFC 9651 verwenden
SunsetDARF NICHT früher alsDeprecationsein — die Spezifikation ist explizit; es ist eine zeitliche Ordnungsbeschränkung- SOLLTE einen
Linkmitrel="deprecation"enthalten, der auf Migrationsdokumentation verweist
Das Senden von Deprecation ändert nicht das Verhalten der Ressource — es ist ein Signal, keine Verschlechterung. Server SOLLTEN die Ressource bis zum Sunset-Datum funktionsfähig halten (abgesehen von Notfällen).
Empfohlener Zeitplan
T+0 Erste Deprecation: Header in Produktion gesendet
T+30d Öffentliche Ankündigung (Changelog, E-Mail, Dokumentation)
T+90d Antwort-Logs/Metriken zur Nutzung des veralteten Pfades hinzufügen
T+180d Sunset-Datum 90 Tage im Voraus festgelegt
T+270d Sunset-Datum erreicht — Endpunkt gibt 410 Gone mit Migrationslink zurück
Für kleinere breaking Changes auf einer auf einer Major-Version fixierten API: 6 Monate sind der Median. Für vollständige v1 → v2-Sunsets: 12-24 Monate.
4. Das Version-Transformer-Pattern
Stripes interne Architektur löst das Problem "wir haben 50 Versionen in Produktion":
[Das System verwendet] API-Ressourcenklassen, die aktuelle API-Antwortstrukturen definieren, kombiniert mit Versionsänderungsmodulen, die rückwärtsinkompatible Transformationen kapseln. Bei der Verarbeitung von Antworten geht das System zeitlich rückwärts und wendet jedes Versionsänderungsmodul an, das es auf dem Weg findet, bis die Zielversion erreicht ist.
Die Form:
// Ressource: die kanonische (neueste) Darstellung
const charge = {
id: 'ch_123',
amount: 1000,
payment_method_details: { card: { brand: 'visa', last4: '4242' } },
}
// Jede breaking Change ist ein Transformer-Modul:
const v_2024_03_01 = {
// Entfernt 'card'-Verschachtelung unter payment_method_details, flacht Felder ab
apply(resource) {
return {
...resource,
card_brand: resource.payment_method_details?.card?.brand,
card_last4: resource.payment_method_details?.card?.last4,
}
},
}
const v_2023_10_15 = {
// Benennt 'amount' in 'amount_cents' um
apply(resource) {
const { amount, ...rest } = resource
return { ...rest, amount_cents: amount }
},
}
// Pipeline wendet Transformer in umgekehrt chronologischer Reihenfolge an,
// bis die angeforderte Version erreicht ist
function transform(resource, requestedVersion) {
const chain = transformers.filter(t => t.date > requestedVersion)
return chain.reduceRight((r, t) => t.apply(r), resource)
}
Die Vorteile:
- Kerncode bleibt modern — jeder Endpunkt wird gegen das neueste Schema geschrieben; keine
if (version < ...)-Abfragen mehr in der Geschäftslogik. - Jeder Bruch ist eine Datei — einfach zu reviewen, einfach isoliert zu testen, einfach zu löschen, wenn der letzte Benutzer darüber hinaus fixiert ist.
- Telemetrie — du kannst zählen, welche Transformer pro Tag ausgelöst werden, um Einstellungen anhand der tatsächlichen Nutzung zu planen.
Der Preis: das Framework für die Transformer zu bauen. Es lohnt sich, wenn du >3 breaking Changes pro Jahr und >10.000 API-Konsumenten hast; für eine <50-Kunden-interne API ist es übertrieben.
5. Interne vs. öffentliche APIs
Die Asymmetrie ist wichtig:
| Intern | Öffentlich | |
|---|---|---|
| Koordinierter Deploy möglich? | Ja — atomarer Austausch | Nein — Clients deployen unabhängig |
| Schema-Evolution | Additiv + Slack-Nachricht | Strenge Versionierung + E-Mail + Dashboards + Blogbeitrag |
| Sunset-Fenster | Tage bis Wochen | 6-24 Monate |
| Versionierungsbedarf | Oft keine — nur Deprecation-Header | Zwingend erforderlich |
| Beste Strategie | Additive Evolution; URI-Version nur bei Rewrite | Datumsbasierte Header (wenn Skala es rechtfertigt), URI für große Rewrites |
Die Falle: Unternehmen behandeln alle APIs als "öffentlich" und bauen die schwere Versionierungsinfrastruktur für eine API, die nur von 3 internen Diensten genutzt wird. Die umgekehrte Falle: Eine durchgesickerte interne API hat externe Konsumenten und du kannst sie eigentlich nicht brechen.
Abwehr: Mache "öffentlich" zu einem binären Tag für den Dienst. Öffentliche Dienste erhalten die vollständige Versionierungszeremonie; interne Dienste nicht, aber erhalten aggressive Überwachung, wer sie aufruft.
Anti-Patterns
| Anti-Pattern | Warum es beißt | Lösung |
|---|---|---|
/v1/ auf /v2/ für eine breaking Change erhöhen | Zwingt Clients, alles für ein Feld zu migrieren | Datumsbasierte Version ODER additive Evolution |
Endpunkte ohne Sunset-Header entfernen | Clients brechen ohne Warnung; Support-Tickets explodieren | Entfernung immer mit Deprecation 90+ Tage vorher kombinieren |
Deprecation ohne Link senden | Clients wissen, dass es veraltet ist, aber nicht, was zu tun ist | Immer Link: <migrations-dok>; rel="deprecation" einfügen |
| Pro Kunde fest codierte Versionsausnahmen | Wird unwartbar; Verhalten nicht nachvollziehbar | Transformer-Pattern verwenden; "Ausnahmen" werden zu Version-Fixes |
| Interne APIs wie öffentliche versionieren | Prozess-Overhead ohne Nutzen | Additive Evolution + atomare Deploys + Slack |
Semver (v1.2.3) auf REST-APIs verwenden | Impliziert Bedeutung, die nicht zutrifft (was ist ein "Patch" für ein JSON-Schema?) | Datumsbasierte Namen oder nur Major (v1) |
| Sunset-Datum ohne Durchsetzung | Endpunkt lebt ewig, Code verrottet | Kalendererinnerung; CI-Test, der 7 Tage vor Sunset fehlschlägt, um Maßnahmen zu erzwingen |
Novize / Experte / Zeitplan
| Novize | Experte | |
|---|---|---|
| Erste breaking Change | Erhöht /v1 auf /v2, kopiert den gesamten Baum | Erwägt datumsbasiertes Pinning, bewertet, ob die Änderung wirklich breaking ist |
| Clients über Einstellung informieren | E-Mail-Explosion | Deprecation + Sunset-Header + Link + E-Mail + Dashboard-Banner |
| 5 Versionen warten | 5 if/else-Verzweigungen in jedem Controller | Transformer-Module, nur neuester Core-Code |
| Sunset-Durchsetzung | "Wir kümmern uns später darum" | Sunset-Datum im CI; Endpunkt wird am Datum zu 410 Gone |
| Interne API-Änderung | Gleiche Zeremonie wie öffentlich — langsam | Additive Evolution + atomarer Deploy; reserviert schwere Prozesse für öffentlich |
Zeitplantest: Wie lange dauert es von "wir wollen Feld X entfernen" bis "Feld X ist in Produktion weg"? Eine Expertenorganisation hat eine einzige wiederholbare Antwort (z.B. 90 Tage). Eine Novizenorganisation hat keine Antwort, weil es keinen Prozess gibt.
Qualitätsgates
Eine Versionierungsänderung wird ausgeliefert, wenn:
- Test: Das Entfernen oder Umbenennen eines extern sichtbaren Feldes wird durch eine mindestens N Tage alte Einstellungsankündigung gesteuert (wobei N das dokumentierte Sunset-Fenster ist).
- Test: Jeder veraltete Endpunkt sendet
Deprecation: @<unix-ts>- undSunset: <HTTP-date>-Header, verifiziert in einem Integrationstest. - Test:
Sunset-Wert ist niemals früher alsDeprecation-Wert (gemäß RFC 9745 MUSS). - Test: Jede Einstellungsankündigung enthält einen
Link: <docs-url>; rel="deprecation"-Header UND ein öffentliches Migrationsdokument, das unter dieser URL existiert (HEAD-Check im CI). - Test: Nutzungstelemetrie pro Version existiert — du kannst beantworten "wie viele Anfragen auf dem v2-Pfad gestern?" bevor du v2 löschst.
- Test (nur datumsbasiert fixiert): Neue API-Keys standardmäßig auf die neueste Version; bestehende Keys behalten ihre Fixierung über Deploys hinweg.
- Manuell: Sunset-Durchsetzungsplan: eine Kalendererinnerung, ein CI-Check oder eine automatisierte 410 Gone — etwas wird ausgelöst, wenn das Datum eintrifft.
NICHT für diese Fähigkeit
- GraphQL-Schema-Evolution (verwende
graphql-schema-evolution) - gRPC / Protobuf-Drahtkompatibilität (verwende
protobuf-evolution-rules) - Datenbank-Schema-Versionierung (verwende
zero-downtime-database-migration) - Bibliotheks-/SDK-Semver (verwende
npm-package-versioning) - Event-Stream-Schema-Evolution (verwende
kafka-schema-registry-design)
Quellen
- Stripe: API-Versionierung bei Stripe — fixierte datumsbasierte Versionen, Transformer-Pattern
- Stripe API: Idempotente Anfragen / Versionierungsrichtlinie
- IETF: RFC 8594 — Der Sunset-HTTP-Header-Feld
- IETF: RFC 9745 — Der Deprecation-HTTP-Header-Feld
- IETF: RFC 9651 — Structured Field Values für HTTP (Datumsformat für Deprecation-Header)
- Roy Fielding: REST-APIs müssen hypertextgesteuert sein — das kanonische Argument gegen URI-Versionierung