Files
claudekit/skills/openapi/references/api-governance.md
T
2026-04-19 14:10:38 +07:00

275 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 1020× 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
<!-- 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)
```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