Zum Inhalt springen
Softwarearchitektur

API-Versionierung: Strategien für langlebige Backends

10 min Lesezeit
API Backend Architektur

Die erste Frage bei API-Versionierung ist nicht “Wie?”, sondern “Muss ich ueberhaupt?”. In den meisten Projekten wird Versionierung eingeführt, bevor es einen einzigen Consumer gibt, der von einem Breaking Change betroffen wäre. Das ist premature abstraction — und sie kostet mehr, als sie bringt.

Warum die meisten Teams zu früh versionieren

Versionierung erzeugt Komplexität. Jede Version ist ein separater Codepfad, der getestet, dokumentiert und gewartet werden muss. Zwei Versionen bedeuten nicht doppelten Aufwand, aber mindestens 40-60% mehr. Drei Versionen und das Team verbringt mehr Zeit mit Kompatibilität als mit Features.

Das YAGNI-Prinzip gilt hier besonders: Solange eine API nur interne Consumers hat — das eigene Frontend, die eigene Mobile-App — braucht man keine formale Versionierung. Man deployed Client und Server zusammen, und fertig. Versionierung wird erst relevant, wenn externe Consumers existieren, die man nicht kontrolliert.

Die drei Strategien im Überblick

1. URL-Pfad-Versionierung

Die einfachste und verbreitetste Variante. Die Version steht direkt in der URL.

URL-Pfad-Versionierung bash
GET /v1/users/42
GET /v2/users/42

# Routing in einem typischen Webframework
# /v1/* -> UsersControllerV1
# /v2/* -> UsersControllerV2

Vorteile:

  • Sofort sichtbar und verstaendlich
  • Einfaches Routing — jede Version ist ein eigener Pfad
  • Caching funktioniert out of the box (unterschiedliche URLs = unterschiedliche Cache-Keys)
  • API-Gateways können trivial nach Version routen

Nachteile:

  • Suggeriert, dass sich die gesamte API geändert hat, auch wenn nur ein Endpoint betroffen ist
  • URLs sind keine Ressourcen-Identifier mehr — /v1/users/42 und /v2/users/42 sind der gleiche User
  • Foerdert “Big Bang”-Versionierung statt granularer Änderungen
  • Clients müssen URLs aktualisieren, nicht nur Header

2. Header-basierte Versionierung

Die Version wird über einen Custom Header mitgeschickt. Der URL-Raum bleibt stabil.

Custom-Header-Versionierung bash
GET /users/42
Accept-Version: v2

# Oder mit einem API-spezifischen Header
GET /users/42
X-API-Version: 2024-01-15

# Server-Antwort enthaelt die verwendete Version
HTTP/1.1 200 OK
X-API-Version: 2024-01-15
Content-Type: application/json

Vorteile:

  • Saubere URL-Struktur — Ressourcen behalten ihre kanonische Adresse
  • Erlaubt datumsbasierte Versionen (wie Stripe es macht)
  • Version kann pro Request variieren, ohne URLs zu ändern

Nachteile:

  • Nicht sichtbar beim Lesen einer URL — Debugging wird schwieriger
  • Custom Headers werden von manchen Proxies und CDNs gefiltert
  • Erfordert explizite Dokumentation, da nicht selbsterklärend
  • Caching erfordert Vary-Header-Konfiguration

3. Content Negotiation

Die Version wird über den Accept-Header als Media Type ausgehandelt. Das ist technisch die “korrekteste” Lösung nach REST-Prinzipien.

Content Negotiation mit Vendor Media Types bash
# Version über Media Type
GET /users/42
Accept: application/vnd.myapi.v2+json

# Oder mit einem Parameter
GET /users/42
Accept: application/vnd.myapi+json;version=2

# GitHub-Stil
GET /repos/octocat/hello-world
Accept: application/vnd.github.v3+json

Vorteile:

  • Nutzt HTTP so, wie es gedacht war
  • Erlaubt gleichzeitig verschiedene Repraesentationen (JSON, XML) und Versionen
  • Granulare Kontrolle pro Ressource

Nachteile:

  • Komplex in der Implementierung
  • Schwer zu testen (curl-Befehle werden unübersichtlich)
  • Die meisten Entwickler sind damit nicht vertraut
  • API-Dokumentationstools unterstützen es schlecht

Breaking vs. Non-Breaking Changes

Bevor man über Versionierung nachdenkt, muss man verstehen, was tatsächlich einen Versionssprung erfordert. Die Antwort: deutlich weniger, als die meisten denken.

Non-Breaking Changes (keine neue Version nötig):

  • Neue Felder in Responses hinzufügen
  • Neue optionale Query-Parameter
  • Neue Endpoints
  • Änderung interner Implementierungsdetails
  • Neue Enum-Werte (wenn der Client unbekannte Werte ignoriert)

Breaking Changes (neue Version nötig):

  • Felder aus Responses entfernen oder umbenennen
  • Pflichtfelder zu Requests hinzufügen
  • Datentyp eines bestehenden Feldes ändern
  • Semantik eines Feldes ändern (gleicher Name, anderes Verhalten)
  • Entfernen oder Umbenennen von Endpoints
  • Änderung von Fehlerformaten

Additive-Only API Design

Die beste Versionierungsstrategie ist oft: keine Versionierung. Stattdessen designt man die API so, dass Breaking Changes gar nicht erst entstehen.

Die Regeln sind einfach:

  1. Nie Felder entfernen. Deprecated Felder liefern weiter Daten (oder Defaults).
  2. Nie Typen ändern. "count": 5 wird nicht zu "count": "5".
  3. Neue Felder sind immer optional. Bestehende Clients senden sie nicht — der Server muss damit umgehen.
  4. Neue Endpoints statt geänderter. /users/42/preferences statt ein geändertes /users/42.
  5. Feature Flags statt Versionen. ?include=extended_profile statt /v2/users/42.
Additive API-Evolution typescript
// Phase 1: Urspruengliche Response
interface UserResponseV1 {
  id: number;
  name: string;
  email: string;
}

// Phase 2: Erweitert, nicht ersetzt
interface UserResponseV2 extends UserResponseV1 {
  // Neue Felder -- bestehende Clients ignorieren sie
  avatar_url: string | null;
  preferences: UserPreferences;
  // Deprecated, aber noch da
  /** @deprecated Use preferences.display_name instead */
  name: string;
}

// Der Client entscheidet, was er braucht
// GET /users/42?fields=id,email,preferences

Deprecation Workflows und Sunset Headers

Wenn eine Version oder ein Endpoint abgelöst wird, braucht es einen klaren Prozess. Der Sunset-Header (RFC 8594) ist dafür das Standardwerkzeug.

Deprecation mit Sunset- und Deprecation-Headern bash
# Server signalisiert: Dieser Endpoint wird am 2026-09-01 abgeschaltet
HTTP/1.1 200 OK
Sunset: Sat, 01 Sep 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/docs/migration-v3>; rel="successor-version"

# Optionaler Warning-Header für zusätzliche Sichtbarkeit
Warning: 299 - "This endpoint is deprecated. Migrate to /v3/users by 2026-09-01"

Ein solider Deprecation-Workflow sieht so aus:

  1. Ankuendigung (6+ Monate vorher): Deprecation-Header setzen, Dokumentation aktualisieren, Changelog schreiben.
  2. Monitoring: Loggen, welche Clients noch die alte Version nutzen. Aktiv auf sie zugehen.
  3. Warnphase (3 Monate vorher): Warning-Header hinzufügen, häufigere Kommunikation.
  4. Sunset: Endpoint liefert 410 Gone mit Link zur neuen Version.

Real-World-Patterns

Stripe: Datumsbasierte Versionen

Stripe nutzt Header-basierte Versionierung mit Datumsversionen (2024-01-15 statt v2). Jede Version ist ein Snapshot des API-Verhaltens an diesem Datum. Neue Accounts bekommen automatisch die neueste Version. Bestehende Accounts bleiben auf ihrer Version, bis sie explizit upgraden.

Das Besondere: Stripe versioniert nicht die gesamte API, sondern einzelne Verhaltensänderungen. Intern werden Änderungen als Feature Flags implementiert, die an Versionen gebunden sind. So kann ein Endpoint in 20 Versionen existieren, ohne dass 20 Codepfade entstehen.

GitHub: Content Negotiation mit pragmatischem Fallback

GitHub nutzt Content Negotiation über den Accept-Header (application/vnd.github.v3+json), aber mit einem wichtigen Pragmatismus: Ohne expliziten Header bekommt man die aktuelle stabile Version. Das senkt die Einstiegshuerde und bestraft nicht Clients, die sich nicht um Versionierung kuemmern.

GitHub ergänzt das durch “API Previews” — experimentelle Features, die über einen speziellen Media-Type aktiviert werden (application/vnd.github.mercy-preview+json). Das entkoppelt Versionierung von Feature-Releases.

API Gateways und Version Routing

In größeren Architekturen übernimmt ein API Gateway das Routing zwischen Versionen. Das hat den Vorteil, dass die Backend-Services nichts von der Versionierung wissen müssen.

Typische Patterns:

  • Path-based Routing: /v1/* geht an Service A, /v2/* an Service B. Einfach, aber erfordert separate Deployments.
  • Header-based Routing: Ein Gateway liest den Version-Header und leitet an den richtigen Service weiter. Flexibler, aber komplexere Gateway-Konfiguration.
  • Transformation Layer: Das Gateway transformiert Requests und Responses zwischen Versionen. Der Service kennt nur die aktuelle Version. Das ist der Stripe-Ansatz — und der mit Abstand wartbarste.

Der Transformation Layer verdient besondere Aufmerksamkeit: Statt zwei Versionen eines Services zu betreiben, schreibt man Transformationsfunktionen, die alte Formate in neue uebersetzen und umgekehrt. Die Logik bleibt zentral, und der Service-Code bleibt sauber.

Praktische Empfehlung

Die richtige Strategie hängt von genau zwei Faktoren ab: wie viele externe Consumers die API hat und wie häufig sich die API ändert.

Interne API (eigenes Frontend/App): Keine Versionierung. Deploy Client und Server zusammen. Investiere in Feature Flags statt in Versionen.

API mit wenigen externen Partnern (unter 10): URL-Pfad-Versionierung. Der Overhead ist minimal, die Vorteile (Sichtbarkeit, einfaches Routing) ueberwiegen. Maximal zwei Versionen gleichzeitig.

API als Produkt (viele externe Consumer): Header-basierte Versionierung mit Datumsversionen, nach dem Stripe-Modell. Investiere in einen Transformation Layer im API Gateway. Das skaliert.

Öffentliche API mit Standards-Anspruch: Content Negotiation. Nur wenn das Team die Komplexität tragen kann und die Consumers sophistiziert genug sind.

Fazit

API-Versionierung ist kein technisches Problem, sondern ein organisatorisches. Die Strategie muss zur Teamgroesse, zur Anzahl der Consumers und zum Änderungsrhythmus passen. Die meisten Teams fahren am besten, wenn sie sich auf Additive-Only Design konzentrieren und Versionierung als Werkzeug der letzten Instanz behandeln. Denn die guenstigste Version ist die, die nie gebaut werden musste.