# 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 `operationId`s, 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:**
1. Spec lives in version control
2. A linter runs on every PR
3. Docs regenerate on merge to main
4. 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:
```yaml
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
```yaml
# .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)
```html
My API
```
### Redoc (static build)
```bash
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)
```ts
// 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.)
```bash
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
```bash
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
```bash
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 diff` or `oasdiff`)
- [ ] Every operation has `operationId`, `tags`, `summary`, at least one `4xx` and at least one `5xx` response
- [ ] 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](rest-naming.md) — URL and naming conventions
- [http-status-codes.md](http-status-codes.md) — status code selection
- [production-patterns.md](production-patterns.md) — idempotency, rate limiting, ETags, webhook signing
- [openapi.tools](https://openapi.tools/) — community catalog of all OpenAPI tools