9.4 KiB
API Governance & Tooling
Linters, docs generators, client generators, mock servers, and contract testing tools for an OpenAPI-based workflow. Focus on what is actually used in 2026.
Why governance matters
Specs drift. A hand-written spec that is not linted in CI will accumulate missing operationIds, inline schemas, undocumented errors, and inconsistent naming within weeks. A generated spec (from FastAPI/NestJS) will drift toward whatever the framework emits, which is rarely idiomatic.
Minimum bar for any new API:
- Spec lives in version control
- A linter runs on every PR
- Docs regenerate on merge to main
- At least one generated client (or contract test) catches breaking changes
Linters
All three below consume the same Spectral-compatible rule format.
| Tool | Language | Speed | When to pick |
|---|---|---|---|
| Spectral (Stoplight) | Node | Moderate | Default choice. Largest rule ecosystem, first-party Redocly + Zalando rulesets. |
| Redocly CLI | Node | Moderate | Pick if you also want bundle, split, and build-docs in one tool. Ships its own opinionated ruleset. |
| Vacuum (daveshanley) | Go | 10–20× faster | Pick for large specs (500+ operations) or monorepo CI where seconds matter. Drop-in Spectral rule compatibility. |
Minimum Spectral ruleset
Save as .spectral.yaml in the spec directory:
extends:
- spectral:oas # Base OpenAPI rules
- spectral:asyncapi # If you mix in AsyncAPI
rules:
# Every operation must be identified, tagged, and summarized
operation-operationId: error
operation-operationId-unique: error
operation-tags: error
operation-summary: error
operation-description: warn
# Schemas must be referenced, not inlined
no-inline-schemas:
description: Request/response bodies must $ref a named schema.
severity: error
given: "$.paths.*.*.requestBody.content.*.schema"
then:
field: "$ref"
function: truthy
# No 3.0-isms in a 3.1 document
no-nullable:
description: "Use type: [T, 'null'] instead of nullable: true in 3.1."
severity: error
given: "$..nullable"
then:
function: falsy
# Enforce camelCase on JSON properties
camel-case-properties:
description: Property names must be camelCase.
severity: error
given: "$.components.schemas..properties[*]~"
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
# Every operation declares at least one 4xx and one 5xx response
operation-4xx-response:
severity: error
given: "$.paths.*.*.responses"
then:
function: schema
functionOptions:
schema:
type: object
patternProperties:
"^4\\d\\d$": {}
minProperties: 1
# Error bodies use application/problem+json
error-uses-problem-json:
description: 4xx/5xx responses must use application/problem+json.
severity: warn
given: "$.paths.*.*.responses[?(@property.match(/^[45]\\d\\d$/))].content"
then:
field: "application/problem+json"
function: truthy
GitHub Actions CI snippet
# .github/workflows/api-spec.yml
name: API spec
on:
pull_request:
paths: ['spec/**']
push:
branches: [main]
paths: ['spec/**']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Bundle spec
run: npx @redocly/cli@latest bundle spec/openapi.yaml -o spec/openapi.bundled.yaml
- name: Lint with Spectral
run: npx @stoplight/spectral-cli@latest lint spec/openapi.bundled.yaml --fail-severity=error
- name: Detect breaking changes vs main
if: github.event_name == 'pull_request'
run: |
git fetch origin main
npx @redocly/cli@latest diff origin/main:spec/openapi.yaml spec/openapi.yaml --fail-on=breaking
The diff --fail-on=breaking step blocks PRs that remove fields, change types, or rename operations — the most common accidental breakages.
Docs Generators
| Tool | Style | When to pick |
|---|---|---|
| Scalar | Modern three-column with built-in REST client | Default choice for new projects. Fast, polished, open source. |
| Redoc | Classic three-column reference (Stripe-like) | Pick when you want the most battle-tested static docs. Works offline. |
| Redocly Portal | Hosted docs with analytics, try-it, versioning | Pick when you need a revenue-class docs portal. Paid. |
| Swagger UI | Interactive try-it | Pick only for internal/debug dashboards. Aesthetics lag behind Scalar/Redoc. |
Scalar (recommended default)
<!-- docs.html -->
<!doctype html>
<html>
<head><title>My API</title></head>
<body>
<script id="api-reference" data-url="/openapi.yaml"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
Redoc (static build)
npx @redocly/cli build-docs spec/openapi.yaml -o dist/api.html
Deploy the single HTML file to any static host (Cloudflare Pages, S3, GitHub Pages).
Client Generation
| Tool | Targets | When to pick |
|---|---|---|
| Kubb | TypeScript (Zod, TanStack Query, SWR, MSW, Axios, Fetch) | Default for 2026 frontend. Plugin-based, generates exactly what you want, no framework bloat. |
| Orval | TypeScript (React Query, SWR, Zod, MSW, Axios) | Close alternative to Kubb. Pick if you prefer a single-config approach. |
| openapi-generator | 30+ languages (Python, Go, Java, Kotlin, Ruby, Rust, etc.) | Default for non-TypeScript languages. The workhorse, but generated code is heavier. |
| openapi-ts (hey-api) | TypeScript only, lightweight | Pick when you want a minimal fetch wrapper with full types and zero framework coupling. |
Kubb starter (TypeScript + TanStack Query + Zod)
// kubb.config.ts
import { defineConfig } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginClient } from '@kubb/plugin-client'
import { pluginReactQuery } from '@kubb/plugin-react-query'
export default defineConfig({
input: { path: './spec/openapi.yaml' },
output: { path: './src/api/generated', clean: true },
plugins: [
pluginOas(),
pluginTs(),
pluginZod(),
pluginClient({ importPath: '../client.ts' }),
pluginReactQuery(),
],
})
openapi-generator (Python / Go / Java / etc.)
npx @openapitools/openapi-generator-cli generate \
-i spec/openapi.yaml \
-g python \
-o clients/python \
--additional-properties=packageName=acme_client,library=asyncio
Mock Servers
| Tool | When to pick |
|---|---|
| Prism (Stoplight) | Run a mock server directly from your spec. Validates requests against the schema and returns examples. Best for frontend dev against an unfinished backend. |
| MSW (Mock Service Worker) | Runs in the browser/Node for testing client code. Pair with Kubb's @kubb/plugin-msw to generate handlers from the spec. |
Prism starter
npx @stoplight/prism-cli mock spec/openapi.yaml --port 4010
# Server at http://127.0.0.1:4010 responds based on the spec examples
Add --errors to make Prism return the declared error responses when the request is invalid, useful for exercising error paths.
Contract Testing
Tools that verify the running implementation still matches the spec.
| Tool | Approach | When to pick |
|---|---|---|
| Schemathesis | Property-based fuzzing driven by the spec | Best signal per line of setup. Catches unhandled edge cases the developer never thought to test. |
| Dredd | Replays documented examples against the server | Simple smoke-test. Good for regression on happy paths. |
| Pact | Consumer-driven contracts (not spec-first) | Pick when consumers write the contracts rather than deriving from the server's OpenAPI. |
Schemathesis in CI
pipx install schemathesis
schemathesis run spec/openapi.yaml \
--base-url=http://localhost:3000/v1 \
--checks=all \
--hypothesis-max-examples=50
Runs ~50 generated requests per operation and checks: status code validity, response schema conformance, Content-Type match, and absence of server errors (5xx).
Governance checklist
Before calling an API "production-ready":
- Spec is in version control alongside the code
- Spec is bundled (
redocly bundle) and the bundled artifact is linted - Spectral (or equivalent) runs on every PR and blocks on errors
- A breaking-change check runs on every PR (
redocly difforoasdiff) - Every operation has
operationId,tags,summary, at least one4xxand at least one5xxresponse - Docs are regenerated on merge to main (Scalar, Redoc, or portal)
- At least one generated client is compiled in CI — proves the spec is consumable
- Contract tests run against a deployed preview before merge
- A mock server (Prism) is available for consumer development
Related
- rest-naming.md — URL and naming conventions
- http-status-codes.md — status code selection
- production-patterns.md — idempotency, rate limiting, ETags, webhook signing
- openapi.tools — community catalog of all OpenAPI tools