API-Versionierungsstrategien: Wann und wie man versioniert
Jede API entwickelt sich weiter – die Frage ist, ob du über Pfade, Header oder Query-Parameter versionierst und wie du Breaking Changes sicher ausrollst.

Die erste Version deiner API wird sich ändern müssen. Felder werden umbenannt, Antwortstrukturen entwickeln sich weiter, und veraltete Endpunkte müssen entfernt werden. Die Frage ist nicht, ob du versionierst, sondern wie du Änderungen einführst, ohne jeden Client zu brechen, der vom aktuellen Vertrag abhängt. Die gewählte Versionierungsstrategie beeinflusst deine URL-Struktur, die Routing-Logik, die Dokumentation und den langfristigen Wartungsaufwand für den Support mehrerer Versionen.
Die drei Ansätze
Versionierung über URL-Pfade
Der expliziteste Ansatz. Die Version ist Teil der URL und in jeder Anfrage sichtbar.
// URL path versioning — /api/v1/users, /api/v2/users
import express from "express";
const app = express();
// V1 routes
const v1Router = express.Router();
v1Router.get("/users", async (req, res) => {
const users = await getUsers();
// V1 returns flat structure
res.json(users.map(u => ({
id: u.id,
name: u.firstName + " " + u.lastName,
email: u.email,
})));
});
// V2 routes
const v2Router = express.Router();
v2Router.get("/users", async (req, res) => {
const users = await getUsers();
// V2 returns nested structure with pagination
res.json({
data: users.map(u => ({
id: u.id,
name: { first: u.firstName, last: u.lastName },
email: u.email,
createdAt: u.createdAt.toISOString(),
})),
pagination: { page: 1, perPage: 20, total: users.length },
});
});
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);Versionierung über Header
Die Version wird in einem Request-Header angegeben. Die URLs bleiben sauber, aber die Version ist in der Browser-Adresszeile und in Logs unsichtbar.
// ❌ Custom header that clients forget to set
// X-API-Version: 2
// ✅ Accept header with vendor media type
// Accept: application/vnd.myapi.v2+json
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
const accept = req.headers.accept ?? "";
const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+json/);
req.apiVersion = match ? parseInt(match[1], 10) : 1; // Default to v1
next();
}
app.get("/api/users", versionMiddleware, async (req, res) => {
const users = await getUsers();
if (req.apiVersion === 1) {
res.json(users.map(u => ({
id: u.id,
name: u.firstName + " " + u.lastName,
email: u.email,
})));
} else if (req.apiVersion === 2) {
res.json({
data: users.map(u => ({
id: u.id,
name: { first: u.firstName, last: u.lastName },
email: u.email,
createdAt: u.createdAt.toISOString(),
})),
pagination: { page: 1, perPage: 20, total: users.length },
});
}
});Versionierung über Query-Parameter
Die Version ist ein Query-Parameter. Einfach zu implementieren, wird aber von Clients leicht vergessen.
// /api/users?version=2
app.get("/api/users", async (req, res) => {
const version = parseInt(req.query.version as string, 10) || 1;
// Route to version-specific handler
});Versionen vermeiden: additive Änderungen
Die beste Version ist keine Version. Viele Änderungen lassen sich additiv umsetzen, ohne bestehende Clients zu brechen.
// ❌ Breaking change — renamed field
// V1: { name: "John Doe" }
// V2: { fullName: "John Doe" } // Every V1 client breaks
// ✅ Additive change — new field alongside old field
// V1: { name: "John Doe" }
// V1.1: { name: "John Doe", fullName: "John Doe" }
// Old clients still read "name", new clients can use "fullName"
interface UserResponseV1 {
id: string;
name: string; // Keep for backward compatibility
email: string;
}
interface UserResponseV1_1 extends UserResponseV1 {
fullName: string; // New field, old clients ignore it
firstName: string; // New field
lastName: string; // New field
}Deprecation-Strategie
Wenn eine Version irgendwann abgeschaltet werden muss, gib den Clients einen klaren Zeitplan und einen Warnmechanismus an die Hand.
// Deprecation middleware — warns clients on old versions
function deprecationMiddleware(version: number, sunsetDate: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (req.apiVersion === version) {
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", sunsetDate);
res.setHeader(
"Link",
'</api/v2/docs>; rel="successor-version"'
);
// Log deprecated version usage for migration tracking
console.log(JSON.stringify({
event: "deprecated_api_call",
version,
path: req.path,
clientId: req.headers["x-client-id"],
sunsetDate,
}));
}
next();
};
}
app.use("/api/v1", deprecationMiddleware(1, "2021-06-01T00:00:00Z"));Versions-Routing mit einem Controller-Pattern
Bei größeren APIs hält ein sauberes Controller-Pattern die versionsspezifische Logik organisiert.
// ❌ Version checks scattered throughout handler code
app.get("/api/users/:id", (req, res) => {
if (req.apiVersion === 1) { /* ... */ }
else if (req.apiVersion === 2) { /* ... */ }
else if (req.apiVersion === 3) { /* ... */ }
// Unmaintainable spaghetti
});
// ✅ Version-specific controllers with shared business logic
// controllers/users/v1.ts
export class UsersControllerV1 {
async getUser(req: Request, res: Response) {
const user = await userService.findById(req.params.id);
res.json(this.serialize(user));
}
protected serialize(user: User) {
return { id: user.id, name: `${user.firstName} ${user.lastName}` };
}
}
// controllers/users/v2.ts
export class UsersControllerV2 extends UsersControllerV1 {
protected serialize(user: User) {
return {
id: user.id,
name: { first: user.firstName, last: user.lastName },
createdAt: user.createdAt.toISOString(),
};
}
}
// Registration
const v1Users = new UsersControllerV1();
const v2Users = new UsersControllerV2();
v1Router.get("/users/:id", (req, res) => v1Users.getUser(req, res));
v2Router.get("/users/:id", (req, res) => v2Users.getUser(req, res));Die wichtigsten Erkenntnisse
- URL-Pfad-Versionierung ist am explizitesten — sichtbar in Logs, einfach zu routen, unkompliziert für Clients
- Additive Änderungen sind neuen Versionen vorzuziehen — Felder hinzuzufügen ist abwärtskompatibel, sie zu entfernen oder umzubenennen nicht
- Setze Deprecation-Header und Sunset-Daten — gib Clients klare Zeitpläne und Migrationspfade
- Nutze Controller-Vererbung für versionsspezifische Logik — halte die gemeinsame Geschäftslogik in der Basisklasse und überschreibe die Serialisierung pro Version
- Tracke die Nutzung veralteter Versionen — wisse, welche Clients noch alte Versionen aufrufen, bevor du sie abschaltest
- Unterstütze höchstens zwei Versionen gleichzeitig — mehr als zwei aktive Versionen zu pflegen erzeugt nicht tragbaren Overhead


