APIs entwickeln sich weiter. Neue Features, Bug Fixes, Performance-Verbesserungen. Aber was, wenn eine Änderung bestehende Clients kaputt macht? Ohne Versionierungsstrategie wird jede Änderung zum Risiko.
Dieser Artikel zeigt, wie du API-Versionen managst, Breaking Changes kommunizierst und alte Versionen graceful abschaltest – ohne Clients im Regen stehen zu lassen.
Zielbild
Nach diesem Artikel kannst du:
- Eine Versionierungsstrategie wählen (URL, Header, Query)
- Breaking von Non-Breaking Changes unterscheiden
- Deprecation mit Sunset-Header kommunizieren
- Migration Guides für Major-Versionen schreiben
- Alte Versionen sicher abschalten
Kernfragen
Bevor du weiterliest, versuche diese Fragen für dein Projekt zu beantworten:
- URL oder Header? Wo steht die Version?
- Was ist ein Breaking Change? Feld entfernen? Typ ändern?
- Wie lange supportet ihr alte Versionen? 6 Monate? 1 Jahr?
- Wie erfahren Clients von Deprecations? E-Mail? Header? Docs?
- Wie oft released ihr neue Versionen? Continuous? Quartalsweise?
Versionierungsstrategien
Drei Hauptansätze haben sich etabliert.
URL Path Versioning
Version im URL-Pfad:
GET /v1/orders HTTP/1.1
GET /v2/orders HTTP/1.1
Vorteile:
- Sofort sichtbar
- Einfach zu testen (Browser, curl)
- Caching-freundlich
- Klare Trennung
Nachteile:
- URL ändert sich bei Major-Version
- Clients müssen URLs anpassen
- Nicht REST-puristisch (Ressource ändert sich nicht)
Empfehlung: Standard für die meisten APIs. Einfach und klar.
Header Versioning
Version im HTTP-Header:
GET /orders HTTP/1.1
Accept: application/vnd.example.v2+json
Oder custom Header:
GET /orders HTTP/1.1
API-Version: 2
Vorteile:
- URL bleibt stabil
- REST-konformer
- Flexibler für Content Negotiation
Nachteile:
- Nicht im Browser testbar
- Clients müssen Header setzen
- Caching komplexer (
Vary: Accept)
Query Parameter Versioning
Version als Query-Parameter:
GET /orders?api-version=2 HTTP/1.1
Vorteile:
- URL bleibt stabil
- Im Browser testbar
Nachteile:
- Query-Parameter für Metadaten unschön
- Kann mit anderen Parametern kollidieren
- Caching komplexer
Entscheidungsmatrix
| Kriterium | URL Path | Header | Query |
|---|---|---|---|
| Sichtbarkeit | Hoch | Niedrig | Mittel |
| Einfachheit | Hoch | Mittel | Mittel |
| Testbarkeit | Hoch | Niedrig | Hoch |
| Caching | Einfach | Komplex | Komplex |
| REST-Konformität | Niedrig | Hoch | Niedrig |
Empfehlung: URL Path Versioning für öffentliche APIs.
Was ist ein Breaking Change?
Ein Breaking Change bricht bestehende Clients.
Breaking Changes (Major Version)
| Änderung | Warum breaking? |
|---|---|
| Feld entfernen | Client erwartet das Feld |
| Feld umbenennen | Client nutzt alten Namen |
| Typ ändern | "price": "10.00" → "price": 10.00 |
| Pflichtfeld hinzufügen | Alte Requests haben es nicht |
| Enum-Wert entfernen | Client nutzt den Wert |
| URL-Pfad ändern | Client nutzt alte URL |
| HTTP-Methode ändern | Client nutzt alte Methode |
| Error-Format ändern | Client parst Fehler |
| Auth-Mechanismus ändern | Client nutzt alten Flow |
Non-Breaking Changes (Minor/Patch)
| Änderung | Warum nicht breaking? |
|---|---|
| Feld hinzufügen (optional) | Clients ignorieren unbekannte Felder |
| Enum-Wert hinzufügen | Clients ignorieren unbekannte Werte |
| Neuer Endpoint | Alte Endpoints funktionieren |
| Optionalen Parameter hinzufügen | Alte Requests funktionieren |
| Beschreibung ändern | Verhalten unverändert |
| Performance verbessern | Verhalten unverändert |
| Bug Fix | War eh kaputt |
Grenzfälle
| Änderung | Breaking? | Empfehlung |
|---|---|---|
| Default-Wert ändern | Kommt drauf an | Dokumentieren, ggf. Breaking |
| Validierung verschärfen | Ja | Breaking, vorher waren Requests gültig |
| Validierung lockern | Nein | Alte Requests funktionieren |
| Nullable machen | Nein | Aber dokumentieren |
| Non-Nullable machen | Ja | Alte Responses hatten null |
| Rate Limit ändern | Kommt drauf an | Lockern OK, verschärfen ankündigen |
Versionsnummern
Semantic Versioning für APIs.
Format (Changelog)
MAJOR.MINOR.PATCH
v2.3.1
| Teil | Wann erhöhen? |
|---|---|
| MAJOR | Breaking Changes |
| MINOR | Neue Features (backward compatible) |
| PATCH | Bug Fixes |
In der URL
Nur Major-Version in URL:
GET /v1/orders HTTP/1.1
GET /v2/orders HTTP/1.1
Nicht:
GET /v1.2.3/orders HTTP/1.1 # Zu granular
Vollständige Version kommunizieren
Response-Header für vollständige Version (custom Header, dokumentieren):
HTTP/1.1 200 OK
API-Version: 2.3.1
Hinweis: Vermeide X- Präfixe (RFC 6648).
Deprecation: Der Prozess
Alte Features und Versionen müssen kontrolliert abgeschaltet werden.
Deprecation-Phasen
Mindest-Zeiträume
| Änderung | Mindest-Vorlaufzeit |
|---|---|
| Feld deprecaten | 3 Monate |
| Endpoint deprecaten | 6 Monate |
| Major-Version deprecaten | 12 Monate |
| Breaking Change | 6 Monate |
Sunset Header (RFC 8594)
Kommuniziert, wann ein Endpoint/Version abgeschaltet wird:
HTTP/1.1 200 OK
Sunset: Sat, 01 Jul 2026 00:00:00 GMT
Deprecation: @1767225600
Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
Sunset verwendet HTTP-date und darf nicht vor dem Deprecation-Datum liegen.
Optional kannst du mit rel="sunset" auf eine Sunset-Policy verlinken.
Deprecation Header (RFC 9745)
Zeigt an, dass etwas deprecated ist:
HTTP/1.1 200 OK
Deprecation: @1767225600
Sunset: Sat, 01 Jul 2026 00:00:00 GMT
Nutze Link: <...>; rel="deprecation" für Migration-Guides oder Hinweise.
Oder mit Timestamp, seit wann deprecated (Structured Field Date):
Deprecation: @1767225600
Deprecation nutzt ein Structured Field Date (@ + Unix-Sekunden).
Implementierung
from datetime import datetime, timezone
from fastapi import Response
DEPRECATED_ENDPOINTS = {
"/v1/orders": {
"deprecated_at": datetime(2026, 1, 1, tzinfo=timezone.utc),
"sunset": datetime(2026, 7, 1, tzinfo=timezone.utc),
"migration_guide": "https://docs.example.com/migration/v2-orders"
}
}
async def deprecation_middleware(request, call_next):
response = await call_next(request)
if request.url.path in DEPRECATED_ENDPOINTS:
config = DEPRECATED_ENDPOINTS[request.url.path]
response.headers["Deprecation"] = f'@{int(config["deprecated_at"].timestamp())}'
response.headers["Sunset"] = config["sunset"].strftime("%a, %d %b %Y %H:%M:%S GMT")
response.headers["Link"] = f'<{config["migration_guide"]}>; rel="deprecation"'
# Logging für Tracking
logger.warning(
"Deprecated endpoint called",
path=request.url.path,
client_id=request.headers.get("X-Client-Id"),
sunset=config["sunset"].isoformat()
)
return response
Migration Guides
Ein guter Migration Guide macht den Umstieg einfach.
Struktur
Ein Migration Guide muss schnell erfassbar sein. Bewährt hat sich diese Struktur:
- Titel mit Versionssprung, z.B.
Migration Guide: v1 → v2 - Überblick mit Sunset-Datum, neuen Features, Breaking Changes und Aufwand
- Breaking Changes als Liste, jeweils mit vorher/nachher und klarer Aktion
- Neue Features mit kurzen Beispielen (optional)
- Checkliste für die Umstellung
- Support-Kanal für Rückfragen
Beispiel: Breaking Changes
1. Feld price ist jetzt eine Number statt String
v1:
{"price": "10.00"}
v2:
{
"price": 10.00
}
Migration:
// Vorher
const price = parseFloat(order.price);
// Nachher
const price = order.price; // Bereits Number
2. Endpoint /orders/list entfernt
v1:
GET /v1/orders/list
v2:
GET /v2/orders
Migration: URL anpassen.
Beispiel: Neue Features
1. Pagination mit Cursor
v2 unterstützt Cursor-Pagination für bessere Performance.
GET /v2/orders?cursor=abc123&limit=20
Beispiel: Checkliste
- [ ] API-Client auf v2 URL umgestellt
- [ ]
priceals Number verarbeiten - [ ] Neue Pagination implementiert (optional)
- [ ] Tests gegen v2 ausgeführt
- [ ] Monitoring für v2 Errors eingerichtet
Support
Fragen? support@example.com oder GitHub Issues.
Changelog (Policy)
Dokumentiere alle Änderungen.
Format
Das Format folgt dem Keep-a-Changelog-Ansatz: Versionsüberschrift mit Datum, darunter die Kategorien (Added, Changed, Deprecated, Removed, Fixed, Security).
Beispiel-Eintrag:
| Kategorie | Beispiel |
|---|---|
| Version | 2.3.0 (2026-01-15) |
| Added | Cursor-Pagination für /v2/orders; neuer Endpoint POST /v2/orders/bulk |
| Changed | Default-Limit für Listen von 20 auf 50 erhöht |
| Deprecated | GET /v1/orders/list; Feld legacy_id in Order-Response |
| Fixed | Rate Limit Header fehlte bei 429 Response |
| Security | CSRF-Token Validierung für Browser-Clients |
Kategorien (Keep a Changelog)
| Kategorie | Inhalt |
|---|---|
| Added | Neue Features |
| Changed | Änderungen an bestehenden Features |
| Deprecated | Features, die bald entfernt werden |
| Removed | Entfernte Features |
| Fixed | Bug Fixes |
| Security | Security-relevante Änderungen |
Parallelbetrieb
Während der Migration laufen alte und neue Version parallel.
Routing
# FastAPI mit Version-Routing
from fastapi import APIRouter
v1_router = APIRouter(prefix="/v1")
v2_router = APIRouter(prefix="/v2")
@v1_router.get("/orders")
async def get_orders_v1():
# Legacy-Implementation
return legacy_format(orders)
@v2_router.get("/orders")
async def get_orders_v2():
# Neue Implementation
return new_format(orders)
app.include_router(v1_router)
app.include_router(v2_router)
Shared Logic
Vermeide Code-Duplikation:
# Shared Business Logic
async def get_orders_core(filters):
return await db.query_orders(filters)
# v1 Formatter
def format_orders_v1(orders):
return [{"id": o.id, "price": str(o.price)} for o in orders]
# v2 Formatter
def format_orders_v2(orders):
return [{"id": o.id, "price": o.price} for o in orders]
# Endpoints
@v1_router.get("/orders")
async def get_orders_v1(filters: Filters):
orders = await get_orders_core(filters)
return format_orders_v1(orders)
@v2_router.get("/orders")
async def get_orders_v2(filters: Filters):
orders = await get_orders_core(filters)
return format_orders_v2(orders)
End-of-Life
Wenn eine Version abgeschaltet wird.
410 Gone
GET /v1/orders HTTP/1.1
HTTP/1.1 410 Gone
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/version-retired",
"title": "API Version Retired",
"status": 410,
"detail": "API v1 was retired on 2026-07-01. Please migrate to v2.",
"error_code": "VERSION_RETIRED",
"migration_guide": "https://docs.example.com/migration/v2"
}
Redirect (Optional)
Für GET-Requests kann ein Redirect helfen:
GET /v1/orders HTTP/1.1
HTTP/1.1 301 Moved Permanently
Location: /v2/orders
Achtung: Nicht für POST/PUT/DELETE – Semantik kann sich geändert haben.
Dokumentation archivieren
Alte Docs nicht löschen, aber klar als archiviert markieren:
⚠️ API v1 wurde am 1. Juli 2026 abgeschaltet.
Diese Dokumentation ist nur noch zu Referenzzwecken verfügbar.
→ Aktuelle Dokumentation: /docs/v2
Monitoring
Tracke Nutzung deprecated Endpoints.
Metriken
DEPRECATED_CALLS = Counter(
"api_deprecated_calls_total",
"Calls to deprecated endpoints",
["route", "version"]
)
# In Middleware
route_obj = request.scope.get("route")
route = route_obj.path if route_obj else "unknown"
DEPRECATED_CALLS.labels(
route=route,
version="v1",
).inc()
Für Client-spezifische Auswertungen nutze Logs oder ein separates Analytics- System, um High-Cardinality in Prometheus zu vermeiden.
Dashboard
| Metrik | Alert |
|---|---|
| Deprecated Calls / Tag | Info wenn > 0 |
| Unique Clients auf deprecated | Warning wenn > 10 |
| Calls nach Sunset-Datum | Critical |
Client-Kommunikation
Wenn ein Client deprecated Endpoints nutzt:
- Automatische E-Mail bei erstem Call
- Wöchentlicher Report mit Nutzung
- Persönliche Kontaktaufnahme 30 Tage vor Sunset
Regeln & Anti-Patterns
Do
- URL Path Versioning für öffentliche APIs
- Nur Major-Version in URL
- Breaking Changes 6+ Monate vorher ankündigen
- Sunset und Deprecation Header verwenden
- Migration Guides schreiben
- Changelog pflegen
- Deprecated-Nutzung tracken
- 410 Gone nach End-of-Life
Don't
- Versionen ohne Ankündigung abschalten
- Breaking Changes als Minor/Patch releasen
- Zu viele aktive Versionen (max 2-3)
- Undokumentierte Breaking Changes
- Migration ohne Guide
- Sunset ohne Vorlaufzeit
- Alte Docs komplett löschen
Artefakt: Deprecation-Policy
# API Deprecation Policy
## Versionierung
- **Schema:** URL Path Versioning (`/v1/`, `/v2/`)
- **Format:** Semantic Versioning (MAJOR.MINOR.PATCH)
- **Aktive Versionen:** Maximal 2 Major-Versionen parallel
## Zeiträume
| Änderung | Mindest-Vorlaufzeit | Ankündigung |
|--------------------------|---------------------|---------------------------|
| Feld deprecaten | 3 Monate | Changelog, Header |
| Endpoint deprecaten | 6 Monate | Changelog, Header, E-Mail |
| Major-Version deprecaten | 12 Monate | Changelog, Blog, E-Mail |
| Breaking Change | 6 Monate | Migration Guide |
## Kommunikation
### Header
Alle deprecated Endpoints/Felder liefern:
```http
Deprecation: @1767225600
Sunset: <HTTP-date>
Link: <Migration Guide URL>; rel="deprecation"
```
### Kanäle
| Phase | Kanal |
|--------------------|------------------------------------------|
| Ankündigung | Changelog, Blog, E-Mail an alle API-User |
| Reminder | Monatliche E-Mail an aktive Nutzer |
| 30 Tage vor Sunset | Persönliche E-Mail an aktive Nutzer |
| End-of-Life | 410 Gone Response |
## Breaking Changes
### Definition
Ein Breaking Change ist:
- Feld entfernen oder umbenennen
- Typ ändern
- Pflichtfeld hinzufügen
- Enum-Wert entfernen
- URL/Methode ändern
- Error-Format ändern
- Auth-Mechanismus ändern
### Prozess
1. Breaking Change identifizieren
2. Neue Version planen
3. Migration Guide schreiben
4. Ankündigung (6+ Monate vorher)
5. Neue Version releasen
6. Alte Version deprecaten
7. Monitoring einrichten
8. Sunset (nach Mindest-Vorlaufzeit)
## Migration Guides (Checkliste)
Jede Major-Version hat einen Migration Guide mit:
- Übersicht der Changes
- Code-Beispiele (vorher/nachher)
- Migrationsschritte
- Checkliste
- Support-Kontakt
## Monitoring (Checkliste)
Wir tracken:
- Calls auf deprecated Endpoints (täglich)
- Unique Clients auf deprecated Endpoints (wöchentlich)
- Nutzung nach Sunset-Datum (Alert: Critical)
## Ausnahmen
In Notfällen (Security, kritische Bugs) können kürzere Zeiträume gelten:
- **Security-Critical:** 24-48 Stunden mit direkter Kommunikation
- **Kritischer Bug:** 7 Tage mit Workaround-Dokumentation
Ausnahmen erfordern Approval von: [API Owner]
## Changelog
Wir führen einen öffentlichen Changelog nach "Keep a Changelog":
- Added, Changed, Deprecated, Removed, Fixed, Security
- Veröffentlicht bei jedem Release
- URL: [https://api.example.com/changelog](https://api.example.com/changelog)
Checkliste (Zusammenfassung)
Bevor du zum nächsten Artikel gehst, prüfe:
- [ ] Versionierungsstrategie ist gewählt (URL empfohlen)
- [ ] Breaking vs. Non-Breaking Changes sind definiert
- [ ] Deprecation-Zeiträume sind festgelegt
- [ ] Sunset und Deprecation Header sind implementiert
- [ ] Migration Guide Template existiert
- [ ] Changelog wird gepflegt
- [ ] Deprecated-Nutzung wird getrackt
- [ ] 410 Gone für retired Versionen
- [ ] Deprecation-Policy ist dokumentiert
- [ ] Kommunikationskanäle sind definiert
Wie es weitergeht
Im nächsten Teil geht es um Dokumentation und Developer Experience: OpenAPI, Beispiele, SDKs und wie du deine API für Entwickler zugänglich machst.
Alle Teile der Serie: Serie: API-Design