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
-
URL-Versionierung nur für Hauptversionen verwenden
/v1/users /v2/users # Nur wenn Breaking Changes unvermeidlich sind -
Innerhalb von Versionen durch additive Änderungen weiterentwickeln
- Neue Felder hinzufügen (alte nicht entfernen)
- Neue Endpunkte hinzufügen
- Neue optionale Parameter hinzufügen
-
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:
- URL-Pfad-Versionierung für Klarheit verwenden
- Innerhalb von Versionen durch additive Änderungen weiterentwickeln
- Neue Versionen nur für Breaking Changes erstellen
- 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.