Checkliste: Contracts, Tests, Fehler, Security, Tooling
Eine OpenAPI-Spezifikation, die nur für die Dokumentation existiert, bringt wenig. Wer sie als bindenden Vertrag zwischen Frontend, Backend und externen Konsumenten behandelt – und sie mit Contract-Tests, RFC-7807-Fehlerformaten, Security-Schemas und automatisiertem Tooling absichert – macht aus einer YAML-Datei ein produktives Qualitätswerkzeug.
Inhaltsverzeichnis
- 1. Warum OpenAPI mehr als Dokumentation ist
- 2. API Contracts: Design-First vs. Code-First
- 3. Contract-Tests mit PHPUnit und Symfony HttpClient
- 4. Fehlerformate: RFC 7807 konsequent anwenden
- 5. Security-Schemas in OpenAPI korrekt modellieren
- 6. Tooling: Spectral, Prism, Stoplight und CI-Integration
- 7. Versionierungsstrategie und Backward Compatibility
- 8. Vergleich: Typische Fehler vs. Best Practices
- 9. Zusammenfassung
- 10. FAQ
1. Warum OpenAPI mehr als Dokumentation ist
OpenAPI wird in vielen Projekten als Dokumentationstool eingesetzt: die Spezifikation entsteht aus Annotations im Code, wird generiert und in einer Swagger-UI angezeigt. Das löst das Problem der fehlenden Dokumentation, verfehlt aber das eigentliche Potenzial. Eine OpenAPI-Spezifikation ist, wenn sie konsequent eingesetzt wird, ein verbindlicher Vertrag zwischen allen Beteiligten: Frontend-Teams wissen exakt, welche Felder und Statuscodes sie erwarten können. Backend-Teams erhalten durch Linting automatische Rückmeldung, wenn sie den Contract verletzen. Externe Konsumenten können Clients generieren, ohne manuell zu dokumentieren.
Der Unterschied zwischen einer Dokumentations-YAML und einem produktiven Contract liegt in drei Punkten: erstens in der Vollständigkeit der Fehlerantworten, zweitens in maschinenlesbaren Security-Schemas und drittens in automatisierten Prüfungen, die sicherstellen, dass der Code dem Contract entspricht. Wer diese drei Punkte systematisch adressiert, verwandelt OpenAPI von einem statischen Artefakt in ein lebendiges Qualitätswerkzeug. Die folgende Checkliste deckt alle relevanten Bereiche ab – von der Contract-Definition über Tests und Fehlerformate bis hin zu Security-Schemas und CI-Integration.
2. API Contracts: Design-First vs. Code-First
Die grundlegende Entscheidung bei der OpenAPI-Integration in Symfony ist, ob die Spezifikation aus dem Code generiert wird (Code-First) oder ob zuerst die YAML geschrieben und dann der Code darunter entwickelt wird (Design-First). In der Praxis hat Design-First entscheidende Vorteile: Frontend- und Backend-Teams können parallel entwickeln, sobald der Contract steht. Die Spezifikation kann vor der ersten Zeile Implementierung durch Reviewer und Konsumenten geprüft werden. Breaking Changes sind früh erkennbar, bevor sie in Code gegossen sind.
Im Symfony-Kontext bedeutet Design-First: die openapi.yaml liegt in config/api/, wird versioniert und ist die einzige Quelle der Wahrheit. NelmioApiDocBundle wird dann nur noch für die Swagger-UI-Anzeige genutzt, nicht für die Generierung. Der Code muss den Contract erfüllen, nicht umgekehrt. Contract-Tests stellen maschinell sicher, dass jede Response dem deklarierten Schema entspricht. So entsteht ein Kreislauf: Contract definieren, Code implementieren, Tests bestätigen die Konformität, CI blockiert bei Abweichungen.
# config/api/openapi.yaml — Design-First: Specification is the source of truth
openapi: "3.1.0"
info:
title: "Mironsoft Shop API"
version: "2.0.0"
contact:
name: "Mironsoft Engineering"
email: "api@mironsoft.de"
servers:
- url: "https://api.mironsoft.de/v2"
description: "Production"
- url: "https://api-staging.mironsoft.de/v2"
description: "Staging"
paths:
/products/{id}:
get:
operationId: getProduct
summary: "Retrieve a single product"
tags: [Products]
parameters:
- name: id
in: path
required: true
schema:
type: integer
minimum: 1
responses:
"200":
description: "Product found"
content:
application/json:
schema:
$ref: "#/components/schemas/Product"
"404":
$ref: "#/components/responses/NotFound"
"401":
$ref: "#/components/responses/Unauthorized"
3. Contract-Tests mit PHPUnit und Symfony HttpClient
Contract-Tests prüfen maschinell, ob die tatsächlichen API-Responses dem deklarierten OpenAPI-Schema entsprechen. Ohne diese Tests kann die Spezifikation vom Code abweichen – und niemand merkt es bis zur Beschwerde eines Konsumenten. Das Werkzeug der Wahl in Symfony ist der HttpClient aus dem Symfony Framework zusammen mit einer JSON-Schema-Validierungsbibliothek wie justinrainbow/json-schema. Der Ablauf: Request senden, Response empfangen, den entsprechenden Schema-Pfad aus der OpenAPI-Datei extrahieren und die Response dagegen validieren.
Ein vollständiger Contract-Test deckt nicht nur den Happy Path ab, sondern auch alle deklarierten Fehlerfälle. Ein 404-Response muss dem ProblemDetails-Schema entsprechen. Ein 422-Response muss ein errors-Array mit Feldinformationen enthalten. Tests, die nur 200-Responses prüfen, geben false security – in der Praxis sind Fehlerresponses die häufigste Quelle von Contract-Verletzungen. PHPUnit-DataProvider ermöglichen es, alle deklarierten Pfade und Statuscodes systematisch zu durchlaufen.
validator = new Validator(new \JsonSchema\Constraints\Factory($schemaStorage));
$raw = file_get_contents(__DIR__ . '/../../../config/api/openapi.yaml');
$this->schema = \Symfony\Component\Yaml\Yaml::parse($raw);
}
public function testGetProductReturns200WithValidSchema(): void
{
$client = static::createClient();
$client->request('GET', '/api/v2/products/1', [], [], [
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTestToken(),
'HTTP_ACCEPT' => 'application/json',
]);
$response = $client->getResponse();
self::assertSame(200, $response->getStatusCode());
$body = json_decode($response->getContent(), false);
$productSchema = $this->schema['components']['schemas']['Product'];
$this->validator->validate($body, (object) $productSchema);
self::assertTrue(
$this->validator->isValid(),
'Response does not match OpenAPI schema: '
. json_encode($this->validator->getErrors())
);
}
public function testGetNonExistentProductReturns404ProblemDetails(): void
{
$client = static::createClient();
$client->request('GET', '/api/v2/products/99999', [], [], [
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTestToken(),
]);
$response = $client->getResponse();
self::assertSame(404, $response->getStatusCode());
self::assertSame('application/problem+json', $response->headers->get('Content-Type'));
$body = json_decode($response->getContent(), true);
self::assertArrayHasKey('type', $body);
self::assertArrayHasKey('title', $body);
self::assertArrayHasKey('status', $body);
self::assertSame(404, $body['status']);
}
private function getTestToken(): string
{
// Returns a pre-generated test JWT for integration tests
return $_ENV['TEST_API_TOKEN'] ?? 'test-token';
}
}
4. Fehlerformate: RFC 7807 konsequent anwenden
RFC 7807 (Problem Details for HTTP APIs) definiert ein standardisiertes JSON-Format für Fehlermeldungen in REST-APIs. Die Felder type (URI), title (menschenlesbar), status (HTTP-Statuscode), detail (kontextspezifisch) und instance (URI der konkreten Anfrage) ermöglichen konsistente Fehlerverarbeitung im Client. Symfony hat ab Version 6.1 native Unterstützung für Problem Details über den ApiPlatform-kompatiblen ErrorController. Wer Api Platform nicht einsetzt, implementiert einen eigenen ExceptionSubscriber, der alle Exceptions in RFC-7807-konforme Responses transformiert.
Das entscheidende Detail: der Content-Type muss application/problem+json sein, nicht application/json. Clients können diesen Header nutzen, um automatisch zwischen normalen Responses und Fehlerantworten zu unterscheiden. Im OpenAPI-Schema wird für jede Fehlerantwort das ProblemDetails-Schema referenziert – und der Contract-Test prüft den Content-Type maschinell. Validation-Fehler (422) erweitern das Schema um ein violations-Array, das pro Feld Pfad, Constraint-Name und Meldung enthält.
5. Security-Schemas in OpenAPI korrekt modellieren
Security-Schemas in OpenAPI sind häufig unvollständig oder inkorrekt modelliert, was dazu führt, dass generierte Clients die Authentication falsch implementieren. OpenAPI 3.1 unterstützt vier Security-Scheme-Typen: apiKey (Header, Query oder Cookie), http (Basic, Bearer), oauth2 (mit vollständigem Flow-Modell) und openIdConnect. Jeder Endpoint muss explizit mit dem passenden Security-Requirement versehen sein – globale Security-Anforderungen gelten zwar für alle Endpoints, aber Ausnahmen (öffentliche Endpoints) müssen explizit mit security: [] deklariert werden.
Im Symfony-Kontext bedeutet das: der security.yaml-Firewall muss exakt das widerspiegeln, was das OpenAPI-Schema deklariert. Eine häufige Diskrepanz: im Schema ist ein Endpoint als öffentlich markiert, aber im Firewall ist er durch einen Voter geschützt – oder umgekehrt. Contract-Tests, die den Endpoint ohne Token aufrufen, decken diese Diskrepanz sofort auf. Für OAuth2-Flows sollten die authorizationUrl, tokenUrl und scopes vollständig im Schema stehen, weil Code-Generatoren diese Informationen für vollautomatische Token-Beschaffung nutzen.
# Security schemas in OpenAPI 3.1 — complete and correct
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT issued by POST /auth/token. Include as:
Authorization: Bearer
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: "Static API key for server-to-server communication"
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: "https://auth.mironsoft.de/oauth/authorize"
tokenUrl: "https://auth.mironsoft.de/oauth/token"
refreshUrl: "https://auth.mironsoft.de/oauth/refresh"
scopes:
"products:read": "Read product catalog"
"products:write": "Create and update products"
"orders:read": "Read own orders"
"orders:write": "Place and cancel orders"
# Apply globally — override per endpoint for public routes
security:
- BearerAuth: []
paths:
/health:
get:
security: [] # Explicitly public — no token required
summary: "Health check endpoint"
6. Tooling: Spectral, Prism, Stoplight und CI-Integration
Ohne automatisiertes Tooling ist eine OpenAPI-Spezifikation schwer konsistent zu halten. Spectral ist ein regelbasierter Linter für OpenAPI-Dokumente, der sicherstellt, dass Style-Guide-Regeln (Naming Conventions, Pflichtfelder, Fehlerformat-Anforderungen) eingehalten werden. Spectral-Regeln werden als YAML definiert und können projektspezifische Anforderungen abbilden: zum Beispiel, dass jeder POST-Endpoint eine 201-Response mit einem Location-Header deklariert, oder dass alle Fehlerantworten das ProblemDetails-Schema referenzieren.
Prism ist ein Mock-Server, der direkt aus der OpenAPI-Spezifikation läuft und reale HTTP-Responses auf Basis der deklarierten Beispiele liefert. Frontend-Teams können gegen den Mock-Server entwickeln, sobald der Contract steht – ohne auf eine Backend-Implementierung zu warten. Prism validiert außerdem eingehende Requests gegen das Schema und liefert aussagekräftige Fehlermeldungen, wenn ein Request nicht konform ist. In der CI-Pipeline läuft Spectral vor jedem Merge, Prism-Validierung läuft als Teil der Contract-Tests.
# .spectral.yaml — Custom API linting rules for the project
extends:
- "spectral:oas"
rules:
# Every POST must declare 201 or 202
post-must-return-201-or-202:
message: "POST endpoints must declare 201 (Created) or 202 (Accepted) response"
severity: error
given: "$.paths[*].post.responses"
then:
function: schema
functionOptions:
schema:
anyOf:
- required: ["201"]
- required: ["202"]
# All error responses must reference ProblemDetails
error-responses-must-use-problem-details:
message: "Error responses (4xx, 5xx) must reference ProblemDetails schema"
severity: warn
given: "$.paths[*][*].responses[4,5]?(*)"
then:
field: "content.application/problem+json"
function: truthy
# Operation IDs must be camelCase
operation-id-camel-case:
message: "operationId must be camelCase"
severity: warn
given: "$.paths[*][*].operationId"
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
7. Versionierungsstrategie und Backward Compatibility
API-Versionierung ist eine der häufigsten Streitfragen im API-Design. URL-Versionierung (/v1/, /v2/) ist explizit und client-freundlich, aber erfordert, dass alle Consumers bei einem Major-Update migrieren. Header-Versionierung (Accept: application/vnd.mironsoft.v2+json) ist HTTP-konformer, aber schwerer zu testen und zu cachen. Eine pragmatische Strategie für Symfony: URL-Versionierung für Major-Versionen, ergänzt durch eine strikte Definition von Breaking Changes.
Als Breaking Change gilt: ein Pflichtfeld wird hinzugefügt, ein Feld wird umbenannt, ein Enum-Wert wird entfernt, ein Statuscode ändert sich, eine URL ändert sich. Als Non-Breaking gilt: ein optionales Feld wird hinzugefügt, ein neuer Endpoint wird hinzugefügt, ein neuer Enum-Wert wird hinzugefügt. Die Prüfung auf Breaking Changes kann automatisiert werden: openapi-diff oder oasdiff vergleichen zwei OpenAPI-Dokumente und markieren Breaking Changes im CI als Fehler. So wird verhindert, dass Breaking Changes in Minor-Versionen einschleichen.
8. Vergleich: Typische Fehler vs. Best Practices
Die folgende Tabelle zeigt die häufigsten Fehler in produktiven OpenAPI-Projekten und das empfohlene Gegenmuster. Viele dieser Fehler entstehen nicht aus Unwissenheit, sondern aus Zeitdruck oder fehlender CI-Automatisierung, die sie abfangen würde.
| Bereich | Typischer Fehler | Best Practice | Auswirkung |
|---|---|---|---|
| Fehlerformat | {"error": "Not found"} |
RFC 7807 ProblemDetails | Clients können Fehler programmatisch verarbeiten |
| Security | Keine Security-Schemas deklariert | Globale + endpoint-spezifische Security | Code-Generatoren implementieren Auth korrekt |
| Fehlerantworten | Nur 200 dokumentiert, 4xx fehlen | Alle deklarierten Statuscodes als Schema | Contract-Tests prüfen auch Fehlerpfade |
| Versionierung | Breaking Changes ohne Major-Version | oasdiff in CI als Breaking-Change-Guard | Konsumenten werden nicht unangekündigt gebrochen |
| Linting | YAML manuell gepflegt, kein Linting | Spectral in CI-Pipeline | Style-Guide-Verstöße werden automatisch erkannt |
9. Zusammenfassung
Eine produktive OpenAPI-Spezifikation in Symfony entsteht nicht durch Annotation-Generierung, sondern durch einen systematischen Ansatz: Design-First definiert den Contract, Contract-Tests bestätigen die Konformität, RFC 7807 standardisiert Fehlerformate, vollständige Security-Schemas ermöglichen korrekte Code-Generierung und Spectral in der CI-Pipeline hält die Qualität automatisch aufrecht. Jedes dieser fünf Elemente ist alleine wertvoll, aber erst in der Kombination entsteht ein System, das Breaking Changes automatisch verhindert und API-Konsumenten zuverlässige Stabilität garantiert.
Der wichtigste organisatorische Schritt ist, die OpenAPI-Spezifikation aus dem Code-Review-Prozess herauszunehmen und in einen eigenen Review-Schritt zu geben – idealerweise mit Frontend-Beteiligung, bevor eine Zeile Implementierung geschrieben wird. Design-First erzwingt diesen Schritt strukturell und macht API-Design zur kollaborativen Entscheidung statt zur Backend-Alleinentscheidung.
Produktive OpenAPI in Symfony — Das Wichtigste auf einen Blick
Design-First
OpenAPI-YAML ist die Quelle der Wahrheit. Code muss den Contract erfüllen, nicht umgekehrt. NelmioApiDocBundle nur für Swagger-UI.
Contract-Tests
PHPUnit + json-schema validiert jede Response gegen das deklarierte Schema. Auch Fehlerpfade (4xx) werden getestet.
RFC 7807 Fehlerformat
Content-Type: application/problem+json. Felder: type, title, status, detail, instance. Violations-Array bei 422.
CI-Tooling
Spectral lint in jedem PR. oasdiff prüft Breaking Changes. Prism als Mock-Server für Frontend-Entwicklung.