API-Versionierungsstrategien, die wirklich funktionieren

Einführung

„Wie sollten wir unsere API versionieren?” ist eine Frage, die überraschend hitzige Debatten auslöst. Nach dem Aufbau und der Pflege von APIs, die von Tausenden von Entwickler:innen genutzt werden, habe ich gelernt, dass der beste Ansatz vom Kontext abhängt – aber manche Strategien sind eindeutig besser als andere.

Warum APIs versionieren?

APIs sind Verträge. Wenn Sie eine API ändern, riskieren Sie, Clients zu brechen, die auf das alte Verhalten angewiesen sind. Versionierung erlaubt es:

  • Die API weiterzuentwickeln, ohne bestehende Clients zu brechen
  • Alte Funktionalität geordnet auslaufen zu lassen
  • Mehrere Client-Versionen gleichzeitig zu unterstützen

Die wichtigsten Ansätze

URL-Pfad-Versionierung

GET /v1/users/123
GET /v2/users/123

Vorteile:

  • Äußerst klar und sichtbar
  • Einfaches Routing auf Load-Balancer-Ebene
  • Einfach umzusetzen
  • Einfach zu dokumentieren

Nachteile:

  • Überlädt URLs
  • Kann zu Code-Duplizierung führen
  • Verleitet dazu, zu viele Versionen zu erstellen

Wann verwenden: Öffentliche APIs, APIs mit vielen externen Konsumenten, wenn maximale Klarheit gewünscht ist.

Query-Parameter-Versionierung

GET /users/123?version=1
GET /users/123?api-version=2023-01-15

Vorteile:

  • Hält URLs sauber
  • Optionaler Parameter (kann standardmäßig auf die neueste Version verweisen)
  • Einfach zu bestehenden APIs hinzuzufügen

Nachteile:

  • Leicht zu vergessen
  • Kann falsch gecacht werden
  • Weniger sichtbar in Logs und Dokumentation

Wann verwenden: Interne APIs, wenn Versionierung optional sein soll.

Header-Versionierung

GET /users/123
Accept: application/vnd.myapi.v1+json

GET /users/123
X-API-Version: 2

Vorteile:

  • Saubere URLs
  • Folgt HTTP-Semantik (Content Negotiation)
  • Trennt Versionierung von der Ressourcen-Identifikation

Nachteile:

  • Bei flüchtiger Betrachtung nicht sichtbar
  • Schwerer im Browser zu testen
  • Komplexere Client-Implementierung

Wann verwenden: Wenn REST-Reinheit wichtig ist, bei anspruchsvollen API-Konsumenten.

Datumsbasierte Versionierung

GET /users/123
Stripe-Version: 2023-10-16

Vorteile:

  • Klare Zeitleiste der Änderungen
  • Fördert schrittweise Weiterentwicklung
  • Keine willkürlichen Versionsnummern

Nachteile:

  • Erfordert Nachverfolgung, was wann geändert wurde
  • Kann verwirrend sein (welches Datum verwende ich?)
  • Größere Änderungen schwerer zu kommunizieren

Wann verwenden: APIs, die sich häufig mit kleinen Änderungen weiterentwickeln (Stripes Ansatz).

Meine Empfehlung

Für die meisten APIs empfehle ich URL-Pfad-Versionierung mit einer Nuance:

Die Strategie

  1. URL-Versionierung nur für Hauptversionen verwenden

    /v1/users
    /v2/users  # Nur wenn Breaking Changes unvermeidlich sind
  2. Innerhalb von Versionen durch additive Änderungen weiterentwickeln

    • Neue Felder hinzufügen (alte nicht entfernen)
    • Neue Endpunkte hinzufügen
    • Neue optionale Parameter hinzufügen
  3. Feature Flags für schrittweise Rollouts verwenden

    GET /v1/users/123?include=new_profile_fields

Warum das funktioniert

  • Klarheit: Entwickler:innen sehen sofort, welche Version sie verwenden
  • Stabilität: Hauptversionen sind selten, sodass Clients nicht oft aktualisiert werden müssen
  • Flexibilität: Additive Änderungen ermöglichen Weiterentwicklung, ohne etwas zu brechen

Änderungen vornehmen, ohne Clients zu brechen

Sichere Änderungen (keine Versionserhöhung)

// Ein neues optionales Feld hinzufügen
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;  // Neues Feld, optional
}

// Einen neuen Endpunkt hinzufügen
GET /v1/users/123/preferences  // Neuer Endpunkt

// Einen neuen optionalen Parameter hinzufügen
GET /v1/users?include_inactive=true  // Neuer Parameter

Breaking Changes (erfordern Versionserhöhung)

// Ein Feld entfernen
// v1: { id, name, email }
// v2: { id, name }  // email entfernt

// Feldtyp ändern
// v1: { age: "25" }  // String
// v2: { age: 25 }    // Zahl

// Endpunktverhalten ändern
// v1: GET /users gibt alle Nutzer:innen zurück
// v2: GET /users gibt paginierte Nutzer:innen zurück

Versionierung umsetzen

Versionierung auf Router-Ebene

// Express-Beispiel
const v1Router = express.Router();
const v2Router = express.Router();

v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);

app.use('/v1', v1Router);
app.use('/v2', v2Router);

Versionierung auf Controller-Ebene

class UserController {
  async getUser(req: Request, res: Response) {
    const version = this.getVersion(req);
    const user = await this.userService.findById(req.params.id);
    
    if (version === 1) {
      return res.json(this.serializeV1(user));
    } else {
      return res.json(this.serializeV2(user));
    }
  }

  private serializeV1(user: User) {
    return {
      id: user.id,
      name: user.name,
      email: user.email
    };
  }

  private serializeV2(user: User) {
    return {
      id: user.id,
      fullName: user.name,  // Umbenanntes Feld
      emailAddress: user.email,
      createdAt: user.createdAt  // Neues Feld
    };
  }
}

Gemeinsame Logik, unterschiedliche Serialisierung

Der Schlüssel ist, Geschäftslogik gemeinsam zu nutzen, während sich der API-Vertrag unterscheidet:

// Gemeinsame Service-Schicht
class UserService {
  async findById(id: string): Promise<User> {
    // Gleiche Logik für alle Versionen
  }
}

// Versionsspezifische Serializer
const serializers = {
  v1: new UserSerializerV1(),
  v2: new UserSerializerV2()
};

// Controller verwendet den passenden Serializer
const serializer = serializers[version];
return res.json(serializer.serialize(user));

Deprecation-Strategie

Frühzeitig kommunizieren

HTTP/1.1 200 OK
Deprecation: Sun, 01 Jan 2025 00:00:00 GMT
Sunset: Sun, 01 Jul 2025 00:00:00 GMT
Link: </v2/users>; rel="successor-version"

Migrationsleitfäden bereitstellen

Dokumentieren Sie genau, was sich geändert hat und wie migriert wird:

## Migration von v1 zu v2

### Änderungen am User-Endpunkt

| v1 | v2 | Anmerkungen |
|----|----|----|
| `name` | `fullName` | Zur Klarheit umbenannt |
| `email` | `emailAddress` | Zur Klarheit umbenannt |
| - | `createdAt` | Neues Feld |

### Erforderliche Code-Änderungen

```diff
- const name = user.name;
+ const name = user.fullName;

### Nutzung überwachen

Verfolgen Sie, welche Versionen verwendet werden:

```typescript
app.use((req, res, next) => {
  const version = extractVersion(req);
  metrics.increment('api.requests', { version });
  next();
});

Häufige Fehler

Zu viele Versionen

Wenn Sie v1, v2, v3, v4, v5… haben, versionieren Sie zu aggressiv. Jede Version verursacht Wartungsaufwand.

Lösung: Additive Änderungen innerhalb von Versionen verwenden. Nur bei wirklich brechenden Änderungen die Version erhöhen.

Uneinheitliche Versionierung

Unterschiedliche Endpunkte verwenden unterschiedliche Versionierungsschemata.

Lösung: Einen Ansatz wählen und ihn konsequent über die gesamte API hinweg beibehalten.

Keine Deprecation-Phase

Alte Versionen ohne Vorwarnung entfernen.

Lösung: Deprecation mindestens 6–12 Monate im Voraus ankündigen. Nutzung vor der Entfernung überwachen.

Versionierung interner APIs

Versionierungs-Overhead für APIs hinzufügen, die nur vom eigenen Team genutzt werden.

Lösung: Interne APIs können sich oft einfach durch koordinierte Deployments weiterentwickeln.

Ein Praxisbeispiel

So habe ich die Versionierung für eine Zahlungs-API strukturiert:

/v1/payments          # Ursprüngliche API (2020)
/v1/payments/intents  # 2021 hinzugefügt, keine Versionserhöhung
/v1/refunds           # 2021 hinzugefügt, keine Versionserhöhung

/v2/payments          # Breaking Changes (2023)
                      # - Betrag von Cent auf Dezimalzahl geändert
                      # - Fehlerantworten umstrukturiert
                      # - Veraltete Felder entfernt

Zeitleiste:

  • 2023-01: v2 angekündigt, v1-Deprecation
  • 2023-06: v2 veröffentlicht, v1 weiterhin unterstützt
  • 2024-01: v1-Sunset-Warn-E-Mails
  • 2024-06: v1 entfernt

Fazit

API-Versionierung muss nicht kompliziert sein:

  1. URL-Pfad-Versionierung für Klarheit verwenden
  2. Innerhalb von Versionen durch additive Änderungen weiterentwickeln
  3. Neue Versionen nur für Breaking Changes erstellen
  4. Geordnet mit langen Vorlaufzeiten auslaufen lassen

Das Ziel ist, den API-Konsumenten Stabilität zu geben, während sich die API weiterentwickeln kann. Eine gut versionierte API schafft Vertrauen und erleichtert die Integration für alle.