Warum eine API-Version keine Bibliotheksversion ist, und wie man den richtigen Sprung automatisiert ableitet
Bei einer Bibliothek entscheidet ein Entwickler beim Release, ob eine Änderung breaking ist, und setzt die Versionsnummer entsprechend hoch. Bei einer REST-API reicht das nicht: Der Vertrag zwischen Server und Dutzenden unbekannten Clients ändert sich, sobald sich die OpenAPI-Spezifikation ändert, und niemand im Team kann von Hand überblicken, welche der zehn parallel laufenden Feature-Branches gerade ein Pflichtfeld entfernt. Dieser Artikel zeigt, wie man Breaking Changes aus OpenAPI-Diffs automatisiert erkennt und den Versionssprung per CI erzwingt, statt ihn dem guten Willen einzelner Entwickler zu überlassen.
Inhaltsverzeichnis
- 1. Warum SemVer bei APIs anders funktioniert als bei Bibliotheken
- 2. Was zählt bei einer REST-API als Breaking Change?
- 3. Minor- und Patch-Änderungen sauber abgrenzen
- 4. OpenAPI-Diffs zur automatisierten Versionsableitung nutzen
- 5. Tooling für die automatisierte Erkennung von Breaking Changes
- 6. Eine CI-Pipeline, die Versionsverstöße verhindert
- 7. URL-Versionierung, Header-Versionierung und Content-Negotiation im Vergleich
- 8. Deprecation- und Sunset-Header richtig einsetzen
- 9. Praxisbeispiel: SemVer-Enforcement in einem Symfony-Projekt
- 10. Zusammenfassung
- 11. FAQ
1. Warum SemVer bei APIs anders funktioniert als bei Bibliotheken
Bei einem npm-Paket oder einem Composer-Package ist der Konsument bekannt: Er hat die Bibliothek explizit in seine composer.json oder package.json eingetragen und kann per Versionsbereich selbst entscheiden, wann er ein Update zieht. Ein Major-Bump bricht dort nichts sofort, denn der Entwickler muss aktiv composer update ausführen, um die neue Version zu erhalten, und kann die Änderung in Ruhe im eigenen Tempo nachvollziehen. Die Versionsnummer ist damit primär eine Information für einen Menschen, der eine bewusste Entscheidung trifft.
Bei einer REST-API ist die Situation umgekehrt: Der Client ruft eine URL auf, und sobald der Server eine neue Antwortstruktur ausliefert, bekommt jeder Aufrufer diese Änderung sofort und ungefragt zu spüren, unabhängig davon, ob er darauf vorbereitet ist. Es gibt keinen Lock-File, der die alte Version festhält, und häufig kennt das Backend-Team gar nicht alle Consumer, insbesondere bei öffentlichen APIs oder bei internen Systemen, die über Jahre organisch gewachsen sind. Genau deshalb muss die Versionsnummer bei einer API nicht nur dokumentieren, sondern aktiv den Zugriff steuern, etwa über einen URL-Pfad oder einen Header, der eine bestimmte Vertragsversion anspricht.
2. Was zählt bei einer REST-API als Breaking Change?
Ein Breaking Change liegt immer dann vor, wenn ein Client, der sich strikt an die bisherige OpenAPI-Spezifikation hält, nach der Änderung nicht mehr funktioniert. Klassische Fälle sind das Entfernen eines Response-Felds, auf das ein Client zugreift, das Umbenennen eines Feldnamens, die Änderung eines Datentyps (etwa von String auf Integer), das Entfernen eines Enum-Werts, auf den ein Client per switch-Anweisung reagiert, oder das Hinzufügen eines neuen Pflichtparameters im Request, den bestehende Clients naturgemäß nicht mitsenden. Auch die Änderung eines HTTP-Statuscodes für denselben Fehlerfall zählt dazu, wenn Clients diesen Code für ihre Fehlerbehandlung auswerten.
Nicht jede auf den ersten Blick riskante Änderung ist tatsächlich breaking. Ein neues optionales Feld in der Response bricht laut REST-Konvention nichts, solange Clients unbekannte Felder ignorieren, was bei jedem sauberen JSON-Parser der Standardfall ist. Ebenso ist ein komplett neuer Endpoint additiv und damit unkritisch, denn kein bestehender Client ruft ihn auf. Die Grenze verläuft also nicht entlang der Frage 'ändert sich etwas', sondern entlang der Frage 'kann ein bestehender, spezifikationskonformer Client dadurch fehlschlagen'. Diese Unterscheidung ist die Grundlage jeder automatisierten Erkennung, denn sie lässt sich strukturell aus dem OpenAPI-Dokument ableiten.
3. Minor- und Patch-Änderungen sauber abgrenzen
Eine Minor-Version signalisiert bei APIs additive, abwärtskompatible Erweiterungen: ein neuer optionaler Query-Parameter, ein zusätzliches Feld in der Response, ein neuer Endpoint oder ein zusätzlicher, optionaler Wert in einem Enum, den alte Clients einfach nicht kennen und daher ignorieren. Eine Patch-Version dagegen ändert den Vertrag überhaupt nicht, sondern korrigiert internes Verhalten: ein Bugfix in der Geschäftslogik, eine Performance-Verbesserung, eine Korrektur eines falschen Rundungsfehlers, solange die OpenAPI-Spezifikation davon strukturell unberührt bleibt. In der Praxis lohnt es sich, diese Grenze direkt im Code sichtbar zu machen, etwa über Attribute an Symfony-Controllern, die die minimale API-Version je Endpoint dokumentieren.
Das folgende Beispiel zeigt, wie sich eine solche Minor-Erweiterung in einem Symfony-Controller sauber markieren lässt, indem der neue, optionale Parameter explizit als seit welcher Version verfügbar dokumentiert wird. Diese Annotation dient nicht nur der Lesbarkeit, sondern kann später von einem eigenen Compiler-Pass oder einem OpenAPI-Generator ausgewertet werden, um automatisch zu prüfen, ob ein als Minor deklarierter Endpoint tatsächlich nur additive Änderungen enthält.
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Attribute\ApiVersion;
use App\Dto\OrderCollectionFilterDto;
use App\Repository\OrderRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
/**
* Liefert eine Liste von Bestellungen für den aktuell authentifizierten
* Mandanten.
*/
final class OrderListController
{
public function __construct(
private readonly OrderRepository $orderRepository,
) {
}
/**
* Gibt die Bestellliste zurück. Der optionale Parameter "status"
* wurde in Version 2.3.0 additiv ergänzt (Minor-Bump), da bestehende
* Clients ohne den Parameter unverändert weiterfunktionieren.
*
* @param Request $request Der aktuelle HTTP-Request
* @return JsonResponse Die JSON-serialisierte Bestellliste
*/
#[Route('/api/v2/orders', methods: ['GET'])]
#[ApiVersion(since: '2.3.0', breaking: false)]
public function __invoke(Request $request): JsonResponse
{
$filter = OrderCollectionFilterDto::fromRequest($request);
$orders = $this->orderRepository->findByFilter($filter);
return new JsonResponse([
'data' => $orders,
'meta' => ['count' => \count($orders)],
]);
}
}
4. OpenAPI-Diffs zur automatisierten Versionsableitung nutzen
Der zuverlässigste Weg, den richtigen Versionssprung zu bestimmen, führt nicht über die Selbsteinschätzung des Entwicklers, sondern über einen strukturellen Vergleich zweier OpenAPI-Dokumente. Man exportiert die Spezifikation des zuletzt veröffentlichten Standes, generiert aus dem aktuellen Branch das neue Dokument und lässt ein Diff-Tool beide Zustände Pfad für Pfad, Parameter für Parameter und Schema für Schema vergleichen. Das Ergebnis ist eine strukturierte Liste von Änderungen, die sich in genau drei Kategorien einordnen lassen: additiv und damit Minor, entfernend oder typ-ändernd und damit Major, oder rein kosmetisch (etwa eine geänderte Beschreibung) und damit ohne Versionsauswirkung.
Aus dieser Klassifikation lässt sich die nächste Versionsnummer rein mechanisch ableiten, genau wie es semantic-release für Bibliotheken aus Commit-Messages tut, nur dass hier nicht der Commit-Text, sondern die tatsächliche Struktur der Schnittstelle die Quelle der Wahrheit ist. Das ist ein entscheidender Vorteil gegenüber Commit-Konventionen wie Conventional Commits, denn ein Entwickler kann sich beim Formulieren einer Commit-Message irren oder schlicht vergessen, das Präfix 'BREAKING CHANGE' zu setzen, während ein struktureller Diff der OpenAPI-Datei diese Änderung unabhängig von der Formulierung zuverlässig erkennt.
5. Tooling für die automatisierte Erkennung von Breaking Changes
Das Werkzeug oasdiff hat sich in den letzten Jahren als De-facto-Standard für OpenAPI-Diffs etabliert, weil es nicht nur textuelle Unterschiede meldet, sondern semantisch versteht, dass etwa eine geänderte Reihenfolge von Properties in einem Schema irrelevant ist, während das Entfernen eines Pflichtfelds kritisch ist. Der Aufruf oasdiff breaking old.yaml new.yaml gibt eine Liste konkreter Breaking Changes mit Dateipfad, betroffenem Endpoint und einer menschenlesbaren Beschreibung zurück, und der Exit-Code des Prozesses signalisiert direkt, ob überhaupt ein Bruch vorliegt, was sich hervorragend für die Automatisierung eignet.
Alternativ bieten sich openapi-diff von OpenAPITools oder kommerzielle Lösungen wie Optic an, die zusätzlich eine Historie über mehrere Versionen hinweg visualisieren und in eine Web-Oberfläche einbetten. Für ein Symfony-Projekt, das seine OpenAPI-Spezifikation über NelmioApiDocBundle generiert, empfiehlt sich ein zweistufiger Prozess: Zunächst wird die Spezifikation als Artefakt bei jedem Merge in den Hauptzweig abgelegt, danach vergleicht ein separater CI-Job diese archivierte Version gegen die des aktuellen Feature-Branchs. So entsteht eine luckenlose Historie, aus der sich jederzeit nachvollziehen lässt, wann welche Änderung eingeführt wurde und ob sie korrekt versioniert war.
6. Eine CI-Pipeline, die Versionsverstöße verhindert
Die Erkennung allein reicht nicht, solange sie nur eine Warnung ausgibt, die im Pull-Request-Kommentar untergeht. Wirksam wird das Vorgehen erst, wenn die CI-Pipeline den Merge aktiv blockiert, sobald ein als Breaking Change erkannter Unterschied vorliegt, ohne dass gleichzeitig die Major-Versionsnummer im Projekt erhöht wurde. Konkret bedeutet das einen Pipeline-Schritt, der drei Dinge prüft: erstens, ob oasdiff breaking Änderungen meldet, zweitens, ob die im Projekt hinterlegte Versionsdatei (etwa eine VERSION-Datei oder ein Composer-Tag) tatsächlich eine neue Major-Version enthält, und drittens, ob im Pull Request explizit ein Label wie breaking-change gesetzt wurde, das eine bewusste menschliche Bestätigung erzwingt.
Fehlt eine dieser drei Bedingungen, schlägt der Build fehl, und der Entwickler muss entweder die Änderung tatsächlich abwärtskompatibel gestalten, etwa durch ein zusätzliches optionales Feld statt eines entfernten Pflichtfelds, oder bewusst die Versionsnummer erhöhen und die Konsequenzen im Pull-Request-Text dokumentieren. Dieser erzwungene Stopp ist unbequem, aber genau das ist der Punkt: Er verhindert, dass ein Breaking Change als vermeintlich harmlose Erweiterung durchrutscht, weil der Entwickler unter Zeitdruck stand oder die Tragweite seiner Änderung schlicht nicht überblickt hat. In größeren Teams zahlt sich dieser Mechanismus schon nach wenigen Wochen aus, sobald er den ersten stillen Produktionsausfall verhindert hat.
7. URL-Versionierung, Header-Versionierung und Content-Negotiation im Vergleich
Die Versionsnummer, die man aus einem OpenAPI-Diff ableitet, muss anschließend irgendwo im tatsächlichen HTTP-Verkehr landen, und dafür gibt es drei etablierte Strategien. Die URL-Versionierung, also ein Präfix wie /api/v2/orders, ist am einfachsten zu implementieren und für Entwickler sofort sichtbar, hat aber den Nachteil, dass jede neue Major-Version faktisch eine zweite, parallel gepflegte Routen-Struktur erfordert und Caching-Layer die Versionsnummer als Teil der URL mitschleppen müssen. Die Header-Versionierung über einen eigenen Header wie Api-Version: 2026-08-07 oder X-API-Version: 2 hält die URL stabil und eignet sich gut für interne APIs, verlangt aber, dass Clients diesen Header aktiv setzen, was leicht vergessen wird.
Content-Negotiation über den Accept-Header, etwa Accept: application/vnd.mycompany.v2+json, gilt in der REST-Community als die 'reinste' Lösung, weil sie dem eigentlichen HTTP-Mechanismus für Formatverhandlung folgt, wird in der Praxis aber selten konsequent durchgehalten, weil sie für Frontend-Entwickler ungewohnt ist und viele HTTP-Clients und Proxys den Accept-Header nicht so granular auswerten wie erwartet. In der Praxis hat sich für die meisten Teams ein Mittelweg bewährt: URL-Versionierung für Major-Versionen, kombiniert mit additiven, abwärtskompatiblen Minor- und Patch-Änderungen innerhalb derselben URL-Version, sodass ein Major-Bump selten genug vorkommt, um den Mehraufwand einer parallelen Routen-Struktur zu rechtfertigen.
8. Deprecation- und Sunset-Header richtig einsetzen
Sobald eine neue Major-Version live ist, beginnt die eigentliche Herausforderung erst: die alte Version muss irgendwann abgeschaltet werden, ohne dass Clients überrascht werden. Der HTTP-Header Deprecation signalisiert, seit wann ein Endpoint als veraltet gilt, während der Sunset-Header (nach RFC 8594) ein konkretes Datum angibt, ab dem der Endpoint nicht mehr erreichbar sein wird. Beide Header lassen sich in Symfony einfach über einen EventSubscriber auf der ResponseEvent-Ebene für alle als deprecated markierten Routen automatisch setzen, sodass jedes Team, das die alte Route noch nutzt, die Information direkt im HTTP-Response sieht, ohne dass jemand aktiv eine E-Mail schreiben muss.
Ergänzend lohnt sich ein Link-Header nach RFC 8288 mit rel="successor-version", der direkt auf die Dokumentation der neuen Version verweist, sodass ein Client-Entwickler beim Debuggen sofort den nächsten Schritt findet. In der Praxis sollte zwischen Deprecation und tatsächlicher Abschaltung ein Zeitraum liegen, der sich an der Release-Frequenz der wichtigsten Consumer orientiert, bei öffentlichen APIs sind sechs bis zwölf Monate üblich, bei internen APIs mit bekannten, wenigen Consumern reichen oft wenige Wochen. Wichtig ist, dieses Datum verbindlich zu kommunizieren und im Sunset-Header technisch abzubilden, statt es nur in einer Wiki-Seite zu vergraben, die niemand liest.
9. Praxisbeispiel: SemVer-Enforcement in einem Symfony-Projekt
In einem konkreten Symfony-Projekt sieht der komplette Workflow so aus: Jeder Merge in den Hauptzweig löst einen GitHub-Actions-Job aus, der zunächst per NelmioApiDocBundle die aktuelle OpenAPI-Spezifikation als YAML-Datei exportiert und als Artefakt archiviert. Bei jedem neuen Pull Request lädt ein weiterer Job die zuletzt archivierte Spezifikation herunter, generiert aus dem Feature-Branch die neue Version und führt oasdiff breaking gegen beide Dateien aus. Meldet das Tool Breaking Changes, prüft ein kleines Shell-Skript, ob die Datei composer.json im selben Pull Request bereits eine neue Major-Version im Feld version enthält, und schlägt andernfalls mit einer klaren Fehlermeldung fehl.
Dieser Aufbau hat sich in der Praxis bewährt, weil er ohne zusätzliche Infrastruktur auskommt, direkt auf vorhandenen Composer- und CI-Mechanismen aufbaut und Entwicklern eine sofortige, verständliche Rückmeldung gibt, statt den Fehler erst in der Produktion sichtbar zu machen. Der wichtigste Effekt zeigt sich indirekt: Weil Breaking Changes durch den erzwungenen Stopp sichtbar und teuer werden, entwickeln Teams mit der Zeit ein Gespür dafür, Änderungen von vornherein additiv zu gestalten, etwa durch neue optionale Felder statt durch das Umbenennen bestehender, was am Ende sogar die Anzahl nötiger Major-Versionen spürbar reduziert.
| Änderungsart | Auswirkung auf Clients | Version-Bump | Beispiel |
|---|---|---|---|
| Neues optionales Response-Feld | Kein Bruch, unbekannte Felder werden ignoriert | Minor | Feld "discountPercentage" ergänzt |
| Neuer Endpoint | Kein Bruch, rein additiv | Minor | POST /api/v2/orders/{id}/cancel |
| Pflichtfeld aus Response entfernt | Bruch, Client-Parsing schlägt fehl | Major | Feld "legacyId" entfernt |
| Enum-Wert entfernt | Bruch, sofern Client per switch reagiert | Major | Status "pending_review" entfällt |
| Bugfix ohne Vertragsänderung | Kein Bruch, nur internes Verhalten korrigiert | Patch | Falsches Rundungsverhalten behoben |
| Neuer Pflichtparameter im Request | Bruch, alte Requests schlagen fehl | Major | Feld "tenantId" wird Pflicht |
Mironsoft
OpenAPI-Design, Symfony-APIs und API-Sicherheit
APIs, die externe Teams ohne Rückfragen integrieren können?
Wir prüfen bestehende REST-APIs auf inkonsistente Fehlerformate, fehlende OpenAPI-Dokumentation und Sicherheitslücken und bauen daraus eine API, die klar dokumentiert, versioniert und gegen Missbrauch abgesichert ist.
API-Review
OpenAPI-Spezifikation, Fehlerformate und Statuscodes auf Konsistenz prüfen.
Symfony-Umsetzung
DTOs, Serializer und Validator für saubere, typsichere Request/Response-Modelle einsetzen.
Security-Audit
Rate-Limiting, Auth-Schemes und Input-Validierung gegen echte Angriffsflächen absichern.
10. Zusammenfassung
Semantic Versioning für APIs: Das Wichtigste auf einen Blick
Patch
Internes Verhalten korrigiert, der OpenAPI-Vertrag bleibt strukturell unverändert.
Minor
Rein additive, abwärtskompatible Erweiterung, bestehende Clients funktionieren unverändert weiter.
Major
Ein spezifikationskonformer Client kann nach der Änderung fehlschlagen.
Tooling
oasdiff vergleicht zwei OpenAPI-Dokumente strukturell und meldet Breaking Changes maschinenlesbar.