# 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