API-Versionierung in PHP: URI, Header und Content-Negotiation
AI generated
<?php
8.4
PHP · API-Versionierung · Breaking Changes · Backend
API-Versionierung in PHP
URI, Header oder Content-Negotiation: welche Strategie wirklich trägt

Ohne durchdachte API-Versionierung wird jeder Breaking Change zum Krisenmeeting mit externen Integratoren. In PHP stehen drei etablierte Ansätze zur Auswahl: Versionsnummer in der URI, ein eigener Versions-Header oder Versionierung über den Accept-Header per Content-Negotiation. Welche API-Versionierung passt, hängt von Kundenstruktur, Release-Rhythmus und der Zahl parallel unterstützter Versionen ab.

17 Min. Lesezeit URI · Header · Content-Negotiation · Deprecation PHP 8.4 · framework-unabhängig

1. Warum API-Versionierung überhaupt nötig ist

Eine API, die von externen Systemen konsumiert wird, kann nicht wie eine interne Bibliothek jederzeit umgebaut werden. Sobald ein Feld umbenannt, ein Endpunkt entfernt oder ein Response-Format geändert wird, bricht jeder Client, der sich auf die alte Struktur verlässt. API-Versionierung löst genau dieses Problem: Sie erlaubt es, eine neue Version parallel zur alten anzubieten, sodass bestehende Integrationen weiterlaufen, während neue Clients bereits die verbesserte Version nutzen.

Ohne API-Versionierung bleiben nur zwei unattraktive Optionen: entweder friert man die API dauerhaft ein, was Weiterentwicklung verhindert, oder man ändert sie und riskiert, dass Partnerintegrationen ohne Vorwarnung ausfallen. Beide Optionen sind in produktiven PHP-Systemen mit externen Konsumenten nicht akzeptabel. Eine durchdachte API-Versionierung schafft den nötigen Spielraum, um Innovation und Stabilität gleichzeitig zu ermöglichen, statt zwischen beidem wählen zu müssen.

2. URI-Versionierung: /v1/, /v2/ in PHP umsetzen

Die verbreitetste Form der API-Versionierung ist die Versionsnummer direkt in der URL, etwa /v1/orders und /v2/orders. Der große Vorteil: Die Version ist auf den ersten Blick sichtbar, lässt sich in Logs, Browser-Adressleisten und API-Dokumentationen ohne zusätzliche Tools nachvollziehen, und funktioniert mit jedem HTTP-Client ohne Sonderbehandlung von Headern. Aus Sicht des Supports ist das ein erheblicher Vorteil, weil ein Fehlerbericht mit URL bereits die genutzte Version enthält.

Der Nachteil dieser API-Versionierung ist, dass sie strenggenommen gegen das REST-Prinzip verstößt, dass dieselbe Ressource unter derselben URI erreichbar sein sollte. /v1/orders/5 und /v2/orders/5 repräsentieren dieselbe fachliche Ressource unter zwei verschiedenen Adressen. In der Praxis überwiegt der pragmatische Nutzen diesen theoretischen Einwand meist deutlich, weshalb URI-Versionierung trotz der Kritik die am häufigsten eingesetzte Form der API-Versionierung bleibt.


<?php

declare(strict_types=1);

// URI-based API versioning: dispatch by version prefix
final class VersionedRouter
{
    /** @var array<string, ControllerInterface> */
    private array $versions = [];

    public function registerVersion(string $version, ControllerInterface $controller): void
    {
        $this->versions[$version] = $controller;
    }

    public function dispatch(string $path, string $method, array $payload): mixed
    {
        if (!preg_match('#^/(v\d+)/(.+)$#', $path, $matches)) {
            http_response_code(400);
            return ['error' => 'Missing API version in path'];
        }

        [, $version, $resource] = $matches;

        if (!isset($this->versions[$version])) {
            http_response_code(404);
            return ['error' => "API version {$version} is not supported"];
        }

        return $this->versions[$version]->handle($resource, $method, $payload);
    }
}

$router = new VersionedRouter();
$router->registerVersion('v1', new OrderControllerV1());
$router->registerVersion('v2', new OrderControllerV2()); // adds pagination cursor

Statt die Version in die URL zu schreiben, kann sie auch über einen eigenen Header übertragen werden, etwa Api-Version: 2. Diese Form der API-Versionierung hält die URL sauber und stabil und behandelt die Version als Metadatum des Requests statt als Teil der Ressourcenadresse, was dem REST-Grundgedanken näherkommt. Für interne Microservices, bei denen alle Clients unter eigener Kontrolle stehen, ist das eine gute Wahl, weil sich Header-Handling zentral in einer HTTP-Client-Bibliothek konfigurieren lässt.

Der Nachteil zeigt sich vor allem bei öffentlichen APIs mit vielen externen Konsumenten: Ein fehlender oder falsch gesetzter Header ist für Entwickler weniger offensichtlich als eine falsche URL, und viele einfache HTTP-Test-Tools, Browser-Adressleisten oder Postman-Collections ohne gepflegte Header-Presets führen schnell zu falsch versionierten Requests. Für interne API-Versionierung zwischen kontrollierten Teams ist der Header-Ansatz dennoch eine der saubersten Lösungen.


<?php

declare(strict_types=1);

// Header-based API versioning: version as request metadata
final class HeaderVersionResolver
{
    public function __construct(
        private readonly string $defaultVersion = '1',
        private readonly array $supportedVersions = ['1', '2'],
    ) {}

    public function resolve(?string $headerValue): string
    {
        $version = $headerValue ?? $this->defaultVersion;

        if (!in_array($version, $this->supportedVersions, true)) {
            http_response_code(400);
            header('Content-Type: application/json');
            echo json_encode([
                'error' => "Api-Version '{$version}' is not supported",
                'supported_versions' => $this->supportedVersions,
            ]);
            exit;
        }

        return $version;
    }
}

$resolver = new HeaderVersionResolver();
$version = $resolver->resolve($_SERVER['HTTP_API_VERSION'] ?? null);

$controller = match ($version) {
    '1' => new OrderControllerV1(),
    '2' => new OrderControllerV2(),
};

4. Content-Negotiation: Versionierung über Accept-Header

Die dritte gängige Strategie der API-Versionierung nutzt Media-Type-Parameter innerhalb des Accept-Headers, etwa Accept: application/vnd.mironsoft.v2+json. Dieser Ansatz gilt vielen als die theoretisch sauberste API-Versionierung, weil er HTTP-Content-Negotiation nutzt, ein Mechanismus, der genau für solche Aushandlungen vorgesehen ist, statt die Versionierung künstlich in URL oder Custom-Header zu pressen.

In der Praxis stößt diese Form der API-Versionierung auf Akzeptanzprobleme: Der Header ist unhandlicher zu schreiben, viele API-Gateways und Caches behandeln Vendor-Media-Types nicht durchgängig korrekt, und die Entdeckbarkeit für neue Entwickler ist geringer als bei einer sichtbaren Versionsnummer in der URL. Für APIs mit strengen Hypermedia- und REST-Reifegradanforderungen bleibt Content-Negotiation dennoch die Variante, die dem REST-Ideal am nächsten kommt, auch wenn sie in der Breite seltener eingesetzt wird als URI-Versionierung.


<?php

declare(strict_types=1);

// Content-negotiation based versioning via vendor media type
final class VendorMediaTypeVersionResolver
{
    public function resolve(string $acceptHeader): string
    {
        if (preg_match('#application/vnd\.mironsoft\.v(\d+)\+json#', $acceptHeader, $matches)) {
            return $matches[1];
        }

        // Fall back to the latest stable version if no vendor type is given
        return '2';
    }
}

$resolver = new VendorMediaTypeVersionResolver();
$version = $resolver->resolve($_SERVER['HTTP_ACCEPT'] ?? '');

header("Content-Type: application/vnd.mironsoft.v{$version}+json");

5. Semantic Versioning für API-Verträge

Semantic Versioning, also die Struktur MAJOR.MINOR.PATCH, stammt ursprünglich aus dem Bibliotheks- und Paketmanagement, lässt sich aber sinnvoll auf API-Versionierung übertragen. Eine MAJOR-Erhöhung signalisiert Breaking Changes, eine MINOR-Erhöhung neue, abwärtskompatible Felder oder Endpunkte, und ein PATCH markiert reine Fehlerbehebungen ohne Verhaltensänderung. In der Praxis wird bei öffentlichen HTTP-APIs meist nur die MAJOR-Version in URL oder Header sichtbar gemacht, weil MINOR- und PATCH-Änderungen per Definition abwärtskompatibel sein müssen.

Diese Konvention schafft eine gemeinsame Sprache zwischen Team und Konsumenten: Wenn die API-Versionierung strikt nach Semantic Versioning kommuniziert wird, wissen Integratoren beim Blick auf ein Changelog sofort, ob ein Update risikofrei eingespielt werden kann oder eine Anpassung des eigenen Codes erfordert. Ohne diese Disziplin verschwimmen Breaking Changes und harmlose Erweiterungen in derselben Versionsnummer, was Vertrauen in die API-Versionierung insgesamt untergräbt.

6. Was überhaupt ein Breaking Change ist

Nicht jede Änderung an einer API rechtfertigt eine neue Hauptversion, aber die Grenze wird in der Praxis oft falsch gezogen. Ein neues optionales Feld in der Response ist kein Breaking Change, weil gut geschriebene Clients unbekannte Felder ignorieren. Das Entfernen eines bestehenden Feldes, das Ändern seines Typs oder das strengere Machen einer zuvor optionalen Validierungsregel sind hingegen klassische Breaking Changes, die eine neue Version in der API-Versionierung erfordern.

Ein besonders unterschätzter Fall: Das Ändern der Reihenfolge von Array-Elementen wirkt harmlos, kann aber Clients brechen, die sich auf Indexpositionen statt auf benannte Schlüssel verlassen. Ebenso zählt das Verschärfen von Rate Limits oder das Einführen einer zuvor optionalen Pflichtfeld-Validierung zu den Breaking Changes, auch wenn keine einzige Zeile der Response-Struktur verändert wurde. Eine saubere API-Versionierung braucht daher eine dokumentierte, für das gesamte Team verbindliche Definition, was als Breaking Change zählt.

7. Deprecation-Workflow: alte Versionen sauber abkündigen

API-Versionierung ohne Deprecation-Strategie führt dazu, dass alte Versionen unbegrenzt weiterlaufen müssen, weil niemand weiß, wer sie noch nutzt. Der erste Schritt ist Sichtbarkeit: Ein Deprecation-Header nach RFC 8594 informiert Clients maschinenlesbar darüber, dass die genutzte Version abgekündigt ist, ergänzt um einen Sunset-Header mit dem geplanten Abschaltdatum. Diese Header lassen sich zusätzlich in jeder Response mitschicken, ohne den eigentlichen Response-Body zu verändern.

Der zweite Schritt ist Monitoring: Jede alte API-Version sollte geloggt werden, inklusive Client-Identifikation, um vor der endgültigen Abschaltung gezielt die verbleibenden Nutzer zu kontaktieren. Eine übliche Frist liegt zwischen sechs und zwölf Monaten zwischen Ankündigung und Abschaltung, abhängig davon, wie kritisch die API für externe Partner ist. Eine API-Versionierung, die diesen Prozess von Anfang an mitdenkt, vermeidet die Situation, alte Versionen aus Angst vor Ausfällen faktisch für immer am Leben halten zu müssen.


<?php

declare(strict_types=1);

// Deprecation headers per RFC 8594, attached without touching the body
final class DeprecationMiddleware
{
    /** @param array<string, array{sunset: string}> $deprecated */
    public function __construct(private readonly array $deprecated) {}

    public function apply(string $version): void
    {
        if (!isset($this->deprecated[$version])) {
            return;
        }

        header('Deprecation: true');
        header("Sunset: {$this->deprecated[$version]['sunset']}");
        header('Link: <https://mironsoft.de/docs/api/v2>; rel="successor-version"');
    }
}

$middleware = new DeprecationMiddleware([
    '1' => ['sunset' => 'Sat, 01 Nov 2026 00:00:00 GMT'],
]);
$middleware->apply($version);

8. Mehrere Versionen in einer PHP-Codebasis pflegen

Die technische Herausforderung bei API-Versionierung liegt selten in der Erkennung der Version, sondern in der Pflege mehrerer paralleler Verhaltensweisen in derselben Codebasis. Ein bewährtes Muster ist, jede Version einen eigenen Controller haben zu lassen, der aber dieselbe Domänenschicht darunter nutzt. Nur die Transformation von Domänenobjekt zu API-Response unterscheidet sich zwischen den Versionen, während Geschäftslogik, Validierung und Persistenz version-unabhängig bleiben.

Ein häufiger Fehler ist, Versionsverzweigungen mit if-Bedingungen quer durch die Geschäftslogik zu streuen, etwa if ($version === 'v1') { ... } in der Mitte einer Service-Methode. Das macht die API-Versionierung schnell unwartbar, weil jede neue Version die Zahl der Verzweigungen in bestehendem Code erhöht. Sauberer ist ein Presenter- oder Serializer-Layer pro Version, der die Domänenlogik unverändert lässt und ausschließlich die Ausgabeform anpasst.

9. API-Versionierung im direkten Vergleich

Die folgende Tabelle stellt die drei Strategien der API-Versionierung mit ihren jeweiligen Stärken und Schwächen gegenüber, als schnelle Entscheidungshilfe für das eigene Projekt.

Strategie Sichtbarkeit REST-Konformität Empfehlung
URI-Versionierung Sehr hoch Umstritten Öffentliche APIs mit vielen externen Konsumenten
Custom-Header Mittel Hoch Interne Microservices unter eigener Kontrolle
Accept-Header / Vendor-Type Gering Sehr hoch APIs mit strengen Hypermedia-Anforderungen
Kein Deprecation-Header Keine Vorwarnung Immer vermeiden, unabhängig von der Strategie

Die Tabelle zeigt: Es gibt keine universell richtige API-Versionierung, sondern eine Abwägung zwischen Sichtbarkeit für externe Entwickler und theoretischer REST-Konformität. Was in jedem Fall Pflicht bleibt, unabhängig von der gewählten Strategie, ist ein sauberer Deprecation-Prozess mit Vorlaufzeit.

Mironsoft

PHP-Backend-Entwicklung und API-Architektur

API-Versionierung, die Breaking Changes nicht zur Krise macht?

Wir entwerfen eine API-Versionierung, die zu eurer Kundenstruktur passt, inklusive Deprecation-Prozess und sauberer Trennung zwischen Domänenlogik und versionsspezifischer Ausgabe.

Strategie-Wahl

URI, Header oder Content-Negotiation passend zur Konsumenten-Struktur auswählen

Deprecation-Prozess

RFC-8594-Header, Monitoring und geplante Abschaltfristen aufsetzen

Codebasis-Refactoring

Presenter-Layer statt Versionsverzweigungen in der Geschäftslogik

10. Zusammenfassung

API-Versionierung ist kein einmaliges technisches Detail, sondern eine langfristige Vertrauensfrage gegenüber jedem, der die API konsumiert. URI-Versionierung bietet die höchste Sichtbarkeit, Header-Versionierung eine sauberere Trennung von Ressource und Metadatum, und Content-Negotiation die theoretisch reinste REST-Lösung. Für die meisten Teams mit externen Konsumenten ist URI-Versionierung der pragmatischste Startpunkt, ergänzt um Semantic Versioning zur klaren Kommunikation von Breaking Changes.

Der entscheidende Erfolgsfaktor liegt weniger in der gewählten Strategie als im begleitenden Prozess: eine klare Definition, was als Breaking Change zählt, ein Deprecation-Workflow mit RFC-8594-Headern und ausreichender Vorlaufzeit, und eine Codebasis, die Versionsunterschiede in einer eigenen Presenter-Schicht kapselt statt sie in der Geschäftslogik zu verteilen. Wer diese drei Bausteine konsequent umsetzt, macht API-Versionierung zu einem planbaren Prozess statt zu einer Serie überraschender Support-Tickets.

API-Versionierung in PHP — Das Wichtigste auf einen Blick

URI vs. Header

URI-Versionierung ist am sichtbarsten, Header-Versionierung sauberer im REST-Sinn. Beides ist in PHP mit wenig Code umsetzbar.

Semantic Versioning

MAJOR für Breaking Changes, MINOR für abwärtskompatible Erweiterungen, PATCH für Fehlerbehebungen ohne Verhaltensänderung.

Deprecation

RFC-8594-Header Deprecation und Sunset geben Konsumenten planbare Vorlaufzeit vor der Abschaltung.

Codebasis

Versionsunterschiede in einer Presenter-Schicht kapseln, Geschäftslogik bleibt version-unabhängig.

11. FAQ: API-Versionierung in PHP

1Welche Strategie für öffentliche APIs?
Meist URI-Versionierung wegen hoher Sichtbarkeit, ergänzt um Semantic Versioning für klare Kommunikation.
2Verstößt URI-Versionierung gegen REST?
Streng genommen ja, praktisch überwiegt aber meist der Nutzen für Sichtbarkeit und Einfachheit.
3Wann ist etwas ein Breaking Change?
Beim Entfernen von Feldern, Typänderungen oder verschärften Validierungen. Neue optionale Felder zählen meist nicht dazu.
4Wie lange alte Versionen unterstützen?
Üblich sind sechs bis zwölf Monate zwischen Ankündigung und Abschaltung, abhängig von der Kritikalität für externe Partner.
5Was macht der Deprecation-Header?
Informiert Clients maschinenlesbar über die Abkündigung, ergänzt um Sunset-Datum, ohne den Response-Body zu verändern.
6Versionsverzweigungen sauber halten?
Presenter-Schicht pro Version nutzen, Geschäftslogik bleibt dabei unverändert und version-unabhängig.
7MAJOR, MINOR, PATCH: was bedeuten sie?
MAJOR für Breaking Changes, MINOR für abwärtskompatible Erweiterungen, PATCH für reine Fehlerbehebungen.
8Content-Negotiation für kleine Teams?
Meist nicht der beste Startpunkt wegen höherer Einstiegshürde, für strenge interne REST-Anforderungen aber geeignet.
9Wer nutzt welche Version?
Konsequentes Logging pro Version und Client-Identifikation, um verbleibende Nutzer vor der Abschaltung gezielt zu kontaktieren.
10Array-Reihenfolge ändern: Breaking Change?
Kann es sein, wenn Clients auf Indexpositionen statt benannte Schlüssel setzen. Dokumentation sollte das explizit klarstellen.