TS
Thomas Schmitz

Freiberuflicher IT-Berater, Softwareentwickler & DevOps. Praxisnahe Artikel zu API-Design, Architektur und Cloud-Workflows.

Serie: API-Design · Teil 17 von 22

API-Design Teil 17: Versionierung & Deprecation

URL vs. Header Versioning, Breaking Changes ankündigen und alte Versionen graceful abschalten.

Praxisnah Checkliste

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:

  1. URL oder Header? Wo steht die Version?
  2. Was ist ein Breaking Change? Feld entfernen? Typ ändern?
  3. Wie lange supportet ihr alte Versionen? 6 Monate? 1 Jahr?
  4. Wie erfahren Clients von Deprecations? E-Mail? Header? Docs?
  5. 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

Phase 1AnkündigungChangelog, Blog,E-MailDeprecation +Sunset-Header inResponsesMigration Guideveröffentlichen6+ MonateÜbergangPhase 2WarningDeprecation-Headerin ResponsesWarnings in LogsDashboardNutzung tracken3+ MonateÜbergangPhase 3End-of-LifeEndpoint gibt 410Gone zurückRedirect zu neuerVersion (optional)DokumentationarchivierenDeprecation-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
  • [ ] price als 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:

  1. Automatische E-Mail bei erstem Call
  2. Wöchentlicher Report mit Nutzung
  3. 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

Mehr Beiträge aus dem Blog.