Spectral: OpenAPI-Definitionen und API-Routen automatisch prüfen
Teams wachsen, APIs wachsen, und irgendwann unterscheidet sich die Spec in einem Repo leicht von der im nächsten. camelCase hier, snake_case dort, Abweichungen von REST oder Error-Responses die keiner gemeinsamen Struktur folgen. Solange alles intern bleibt, fällt das kaum auf. Wenn aber generierter Client-Code oder externe Consumer ins Spiel kommen, werden aus kleinen Abweichungen echte Probleme.
OpenAPI v3 ist heute der De-facto-Standard für REST-API-Dokumentation, aber eine valide Spec ist noch keine konsistente Spec. Schema-Validatoren prüfen, ob das YAML dem OpenAPI-JSON-Schema entspricht. Was sie nicht prüfen, sind eigene Konventionen: Namensregeln, Pflichtfelder, Response-Formate. Genau hier setzt Spectral an.
Warum API-Design-Konsistenz ein echtes Problem ist
Im Postman State of the API Report gehören veraltete und inkonsistente Dokumentation zu den meistgenannten Problemen bei der API-Integration. Das ist kein Zufall. Konventionen entstehen oft implizit: Ein Team nutzt Plural-Routen, ein anderes Singular, ein drittes hat POST /users/search statt GET /users?filter=... eingeführt, weil die Filtermenge zu groß für Query-Parameter war. Diese pragmatischen Abweichungen von purem REST sind in der Praxis normal. Sie werden dann zum Problem, wenn sie undokumentiert sind und von Team zu Team variieren.
Konkret zeigt sich das bei fehlenden Operation IDs für die Routen, die generierte Clients zu autogenerierten Funktionsnamen zwingen, bei unterschiedlichen Pagination-Formaten im selben Produkt oder bei Error-Responses, die in einem Endpunkt einem eigenen Schema und in einem anderen dem RFC-7807-Format (Problem Details) folgen. API-Consumer verlassen sich auf die Spec, und wenn die Spec nicht das widerspiegelt, was der Code tatsächlich liefert, ist das Vertrauen weg.
OpenAPI v3 löst das nicht allein. Eine Spec kann strukturell korrekt und gleichzeitig inkonsistent sein. Linter brauchen Regeln, nicht nur Schema-Validierung.
Was ist Spectral?
Spectral ist ein Open-Source JSON/YAML-Linter von Stoplight. Er unterstützt OpenAPI 2.0 (Swagger), OpenAPI 3.0, OpenAPI 3.1, AsyncAPI und beliebige YAML/JSON-Strukturen. Spectral ist kein Runtime-Validator, er braucht keinen laufenden Server, keine Requests und keine Responses. Er analysiert statisch, was ihn ideal für den CI-Einsatz macht.
Das Kernmodell besteht aus drei Konzepten. Ein Ruleset ist eine Sammlung von Regeln, die aus lokalen Dateien, NPM-Packages oder URLs eingebunden werden kann. Eine Rule besteht aus einem given-JSONPath (welcher Teil der Spec wird geprüft), einer then-Funktion mit Optionen (was wird geprüft) und einer severity (error, warn, info, hint). Functions sind entweder Built-in (z.B. truthy, pattern, casing, schema, length) oder als eigene TypeScript-Dateien implementiert.
Der Unterschied zu anderen Tools ist klar: openapi-schema-validator prüft nur Struktur. express-openapi-validator prüft Requests und Responses zur Laufzeit. Spectral prüft semantische Konventionen zur Entwicklungszeit, bevor irgendetwas deployed ist.
Spectral installieren und OpenAPI-Dateien linten
Für den Projekteinsatz empfiehlt sich die Installation als Dev-Dependency, damit alle Entwickler und die CI dieselbe Version nutzen:
# Installation als Dev-Dependency (empfohlen für CI-Konsistenz)
npm install --save-dev @stoplight/spectral-cli
# Lint einer OpenAPI-Datei
npx spectral lint ./openapi/api.yaml
# Mit explizitem Ruleset
npx spectral lint ./openapi/api.yaml --ruleset .spectral.yaml
Die Konfiguration liegt in einer .spectral.yaml im Repo-Root. Das Built-in-Ruleset spectral:oas deckt bereits viele sinnvolle Basisregeln für OpenAPI ab. Eigene Regeln kommen darunter:
# .spectral.yaml – minimales Beispiel
extends:
- spectral:oas # Built-in OpenAPI Ruleset
rules:
operation-id-required:
message: 'Jede Operation muss eine operationId haben.'
given: '$.paths[*][*]' # Trifft auf alle HTTP-Methoden in allen Pfaden zu
severity: error
then:
field: operationId
function: truthy
Spectral gibt Fehler mit Dateipfad, Zeile, Spalte, Severity und Regelname aus:
/openapi/api.yaml
12:5 error operation-id-required Jede Operation muss eine operationId haben.
paths./users.post
Der Exit Code ist 0 bei Erfolg und 1 bei gefundenen Problemen, was die CI-Integration direkt möglich macht. Mit --fail-severity lässt sich der Schwellwert anpassen: --fail-severity warn lässt den Job auch bei Warnungen fehlschlagen.
Code und Spec in Sync halten
Eine häufige Situation: Die Spec liegt im Repo, aber der Code driftet irgendwann davon ab. Ein neuer Endpunkt wird schnell hinzugefügt, die Spec-Aktualisierung folgt nicht. Hier gibt es zwei grundsätzliche Strategien.
Bei Code-First wird die OpenAPI-Spec aus dem Code generiert. Tools wie Swashbuckle für .NET oder swagger-autogen für Express und Node.js erzeugen eine statische Spec-Datei, welche an Spectral zur Analyse übergeben wird:
# Code-First: OpenAPI generieren und sofort linten (.NET mit Swashbuckle)
dotnet tool install -g Swashbuckle.AspNetCore.Cli
swagger tofile --output ./openapi/api.json ./bin/Release/net8.0/MyApi.dll v1
npx spectral lint ./openapi/api.json
Bei Spec-First liegt die kanonische Spec im Repo und kann dort direkt von Spectral geprüft werden. Der Code wird dann so implementiert, dass er die Spec erfüllt. Das erfordert Disziplin, aber es stellt sicher, dass die Spec immer aktuell und korrekt ist.
Für Teams mit einem bewussten API-Design-Prozess ist Spec-First die bessere Wahl. Code-First eignet sich gut für schnelles Bootstrapping oder wenn die Spec primär zur Dokumentation nach außen dient.
Community-Rulesets einbinden
Neben dem Built-in-Ruleset gibt es fertige Community-Rulesets für gängige Governance-Anforderungen. Das Azure API Style Guide Ruleset erzwingt Azure-typische Konventionen: OperationId-Format, Error-Response nach RFC 7807, Paginierungs-Pattern und HTTP-Methoden-Konventionen. Das Ruleset @stoplight/spectral-owasp-rules prüft auf Basis der OWASP API Security Top 10, ob Authentifizierungsschemata fehlen, Rate-Limit-Header gesetzt sind und Schemas sinnvolle Größenbeschränkungen haben.
Rulesets lassen sich kombinieren und selektiv überschreiben. Die Reihenfolge in extends ist dabei wichtig: spätere Einträge überschreiben frühere:
# .spectral.yaml – Azure Ruleset einbinden
extends:
- spectral:oas
rules:
# Eine Azure-Regel auf Warnung herabstufen
OperationIdNounVerbConvention:
severity: warn
# Eine Regel komplett deaktivieren
LroStatusCodesReturnTypeSchema:
severity: off
Alternativ kannst du ein Ruleset direkt per URL einbinden, ohne lokale NPM-Installation:
extends:
- https://raw.githubusercontent.com/azure/azure-api-style-guide/main/spectral.yaml
Die Extending Rulesets Dokumentation erklärt, wie Overrides granular auf einzelne Dateien oder Bereiche angewendet werden können.
Eigene Regeln und Functions schreiben
Die Built-in-Functions decken viele Fälle ab. Für firmenspezifische Konventionen, die sich nicht mit pattern oder casing ausdrücken lassen, schreibst du eine Custom Function. Das Beispiel erzwingt PascalCase für alle Tag-Namen:
# .spectral.yaml – Custom Function referenzieren
functions:
- ./functions/tag-pascal-case
rules:
tag-naming-pascal-case:
message: 'Tags müssen in PascalCase geschrieben sein (z.B. UserManagement).'
given: '$.tags[*].name'
severity: error
then:
function: tag-pascal-case
// functions/tag-pascal-case.ts
import { createRulesetFunction } from '@stoplight/spectral-core';
export default createRulesetFunction<string, null>(
{
input: { type: 'string' },
options: null,
},
(input) => {
// Gibt ein Fehler-Array zurück bei Verletzung, undefined bei Erfolg
if (!/^[A-Z][a-zA-Z0-9]*$/.test(input)) {
return [{ message: `'${input}' ist kein gültiges PascalCase-Tag.` }];
}
},
);
Custom Functions lassen sich mit Jest testen. Das ist besonders wichtig, wenn die Regeln Teil eines geteilten Rulesets sind:
// __tests__/tag-pascal-case.test.ts (Jest)
import { Spectral } from '@stoplight/spectral-core';
import { bundleAndLoadRuleset } from '@stoplight/spectral-ruleset-bundler/with-loader';
import * as path from 'path';
it('meldet Fehler bei snake_case Tag-Namen', async () => {
const spectral = new Spectral();
const ruleset = await bundleAndLoadRuleset(
path.join(__dirname, '../.spectral.yaml'),
{ fs, fetch },
);
spectral.setRuleset(ruleset);
const results = await spectral.run({
openapi: '3.0.3',
info: { title: 'Test', version: '1.0.0' },
tags: [{ name: 'user_management' }],
paths: {},
});
expect(results).toContainEqual(
expect.objectContaining({ code: 'tag-naming-pascal-case' }),
);
});
Spectral als Merge-Gate
Die Spectral CLI unterstützt mehrere Output-Formate: stylish (Standard, für Menschen lesbar), github-actions (für Inline-Annotationen im PR), sarif (für den GitHub Code Scanning Tab) und junit. In einem GitHub Actions Workflow kombinierst du das github-actions-Format für direkte PR-Annotationen mit einem SARIF-Upload:
# .github/workflows/spectral-lint.yml
name: API Spec Linting
on:
pull_request:
paths:
- 'openapi/**'
- 'src/api/**'
- '.spectral.yaml'
jobs:
lint-openapi:
name: Lint OpenAPI Spec
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Spectral lint
run: |
npx spectral lint openapi/api.yaml \
--format github-actions \
--output results.sarif \
--format sarif
continue-on-error: false # PR blockieren bei Fehlern
- name: Upload SARIF results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
category: spectral-openapi
Der paths-Filter stellt sicher, dass Spectral nur bei Änderungen an Spec-Dateien oder der Ruleset-Konfiguration läuft. Das hält die CI-Laufzeit kurz. Spectral ist schnell, bei großen Specs in der Regel unter zehn Sekunden. Für lokales Feedback vor dem Push eignet sich ein Pre-Commit Hook:
# .husky/pre-commit
#!/bin/sh
npx spectral lint openapi/api.yaml --fail-severity warn
Die Spectral GitHub Action im Marketplace ist eine Alternative zum direkten CLI-Aufruf, bietet aber weniger Konfigurationsfreiheit. Wer spezifische Output-Formate oder mehrere Spec-Dateien in einem Job linten will, ist mit npx spectral lint besser bedient.
Inkrementelle Einführung und Stolpersteine
Der häufigste Grund, warum Spectral-Einführungen stagnieren, sind zu viele Fehler auf einmal. Wenn eine bestehende Spec 150 Regelverstöße erzeugt, ist das entmutigend. Die empfohlene Strategie: Starte mit severity: warn für alle Regeln und --fail-severity error in CI. So siehst du alle Verstöße, aber nur echte Errors blockieren den PR. Hebe dann schrittweise Regeln auf error an.
Für Einzelausnahmen gibt es zwei Wege. x-spectral-rule-ignore als Extension direkt am YAML-Knoten unterdrückt eine Regel lokal:
paths:
/legacy/endpoint:
x-spectral-rule-ignore:
- operation-id-required # TODO: Wird im nächsten Sprint ergänzt
get:
responses:
'200':
description: 'OK'
Für datei- oder pfadspezifische Anpassungen nutzt du overrides im Ruleset:
# .spectral.yaml
overrides:
- files:
- 'openapi/legacy/**'
rules:
operation-id-required:
severity: warn # Nur für Legacy-Specs herabgestuft
Bei Multi-Team-Setups lohnt sich ein zentrales NPM-Package mit dem geteilten Ruleset. Teams binden es per extends: '@myorg/spectral-rules' ein und können projektspezifische Regeln darunter ergänzen. Die Ownership des Packages sollte klar definiert sein, damit Änderungen an zentralen Regeln koordiniert eingeführt werden.
Wer Spectral mit Redocly CLI vergleicht: Redocly ist ähnlich mächtig, stärker auf die Redocly-Toolchain ausgerichtet und kommerziell orientierter. Spectral ist die offenere, community-getriebene Wahl mit einem größeren Ökosystem an fertigen Rulesets.
Spectral ersetzt keine Laufzeit-Validierung. Schemathesis findet Abweichungen durch property-based Testing gegen eine OpenAPI-Spec zur Laufzeit. jest-openapi prüft Response-Objekte in Integration Tests gegen die Spec. Beide Ebenen adressieren unterschiedliche Fehlerklassen und ergänzen sich gut.
API-Design-Governance nachhaltig machen
Spectral ist kein Allheilmittel. Ein Linter kann nur Konventionen durchsetzen, die vorher definiert wurden. Wenn das Team keine schriftlichen API-Guidelines hat, helfen Regeln wenig. Der Linter ist das Durchsetzungswerkzeug, die Guidelines sind das Fundament.
Der pragmatische Einstieg geht in drei Schritten: Gemeinsames Ruleset finden, aktivieren und als nicht-blockierenden CI-Job anlegen. Danach eine teamspezifische Regel hinzufügen, die das dringendste aktuelle Problem adressiert, z.B. fehlende operationId oder inkonsistente Error-Responses. Nach einem Sprint die kritischsten Regeln auf error anheben und schrittweise erweitern. Was dabei konstant sichtbar bleibt: API-Qualität als messbares Ziel, nicht als Bauchgefühl.
Wie setzt du API-Governance in deinem Projekt um? Schau gerne bei uns auf LinkedIn vorbei und diskutiere mit.