Keep external API contracts documented from a single source of truth (docs/openapi.yml),
with generated HTML, CI validation, and a recyclable guide so agents update docs alongside code.
- The app exposes a REST API or webhooks consumed by external systems.
- Documentation today is fragmented (scattered markdown, ad-hoc Swagger, a docs gem/lib).
- You want to publish to SwaggerHub and/or ship a static HTML reference (Redoc).
- Any change to a public route, request/response shape, auth scheme, or error contract.
| File | Role |
|---|---|
docs/openapi.yml |
SSOT — OpenAPI 3.0, the only file edited by hand |
public/api-docs.html (or docs/api-docs.html) |
Generated HTML (Redoc). Never edit by hand |
.context/decisions/NNN-openapi-as-api-documentation-ssot.md |
ADR recording the decision |
<task runner> task |
docs:validate + docs:build (see per-stack examples) |
| CI step | Runs docs:validate on every push |
Rule: docs/openapi.yml is the contract. The HTML is a build artifact. SwaggerHub is for
mock/collaboration. Do not introduce a second source of docs.
Document only what external consumers depend on. Examples:
POST /api/v1/...—Authorization: Bearer <API_KEY>GET /api/v1/...— BearerPOST /webhooks/...— per-integration header (e.g.X-<Source>-Webhook)
Do not document: admin pages, sessions/login, internal UI endpoints, background jobs (Sidekiq/Celery/queues), health checks — unless they are a published public contract.
openapi: 3.0.0
info:
title: "{App} API"
description: |
External contracts. Auth:
- REST API: Authorization: Bearer {API_KEY}
- Webhooks: integration-specific headers
version: 1.0.0
servers:
- description: SwaggerHub Auto Mocking
url: https://virtserver.swaggerhub.com/{ORG}/{APP}/1.0.0
- description: Production
url: https://{host}
paths: {}
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
# one apiKey scheme per webhook header, mirroring the controllers
schemas: {}validate checks the YAML parses and has the minimum structure (openapi, non-empty paths).
build renders docs/openapi.yml into self-contained HTML via the Redoc standalone bundle.
Adapt to the project's task runner — the logic is identical across stacks.
Ruby / Rails (lib/tasks/docs.rake)
namespace :docs do
OPENAPI_PATH = Rails.root.join("docs/openapi.yml")
HTML_PATH = Rails.root.join("public/api-docs.html")
desc "Validate OpenAPI spec (YAML + minimal structure)"
task validate: :environment do
spec = YAML.load_file(OPENAPI_PATH)
abort "[docs] invalid: missing 'openapi'" unless spec.is_a?(Hash) && spec["openapi"].present?
abort "[docs] invalid: empty 'paths'" unless spec["paths"].is_a?(Hash) && spec["paths"].any?
puts "[docs] OK (#{spec['paths'].size} paths)"
end
desc "Generate HTML from OpenAPI (Redoc CDN)"
task build: :validate do
spec = YAML.load_file(OPENAPI_PATH)
html = <<~HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>#{ERB::Util.html_escape(spec.dig('info', 'title') || 'API')}</title>
</head>
<body>
<div id="redoc-container"></div>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
<script>
Redoc.init(#{JSON.generate(spec)}, {}, document.getElementById("redoc-container"));
</script>
</body>
</html>
HTML
File.write(HTML_PATH, html)
puts "[docs] wrote #{HTML_PATH}"
end
endNode (package.json scripts)
Python (Makefile or scripts)
docs-validate:
python -c "import yaml,sys; s=yaml.safe_load(open('docs/openapi.yml')); \
sys.exit(0 if s.get('openapi') and s.get('paths') else 'invalid spec')"
docs-build:
npx @redocly/cli build-docs docs/openapi.yml -o public/api-docs.htmlNo separate workflow needed. build runs locally or at deploy time.
- name: Validate OpenAPI spec
run: <task-runner> docs:validate # e.g. bundle exec rake docs:validate / npm run docs:validateWhen code changes touch public contracts, map the change to the spec. Never invent fields — read the controller/handler and its param permit-list / validations / serializer first.
| Code change | Edit in OpenAPI |
|---|---|
Routes (/api/v1/*, /webhooks/*) |
paths — exact method + path |
| API controllers/handlers | requestBody, parameters, security: BearerAuth |
| Webhook controllers/handlers | security (header scheme) + body schemas |
| API auth concern/middleware | components.securitySchemes + 401 response |
| Webhook auth concern/middleware | header scheme + 401 response |
| Event-type enums | enum in the schema |
| Error message i18n/strings | example in the responses |
Steps:
- Read the controller/handler + its
params.permit/ schema / validations (source of truth for fields). - Edit
docs/openapi.yml— updatepathsandcomponents/schemas. - Write descriptions in the project's documentation language; put real error messages in
examples. - YAML strings containing
:must be quoted —'Error: field X is required'. - Run
docs:validate→ thendocs:build. - Commit
docs/openapi.ymland the generated HTML together.
- Duplicating the contract in markdown.
- Documenting internal routes (admin, sessions, UI, jobs).
- Editing the generated HTML by hand.
- Adding a second docs source (a docs gem/lib, a parallel Swagger file).
- Inventing request/response fields not present in the code.
In skills that add an endpoint / webhook / event, add a final step:
### N. Update OpenAPI documentation
Follow `.claude/skills/update-api-documentation/SKILL.md`.In CLAUDE.md / project rules:
OpenAPI SSOT:
docs/openapi.yml. When changing/api/v1or/webhooks, follow theupdate-api-documentationskill, then rundocs:validateanddocs:build.
- Inventory public routes (REST + webhooks).
- Read controllers/handlers + auth concerns/middleware.
- Create
docs/openapi.ymlwithpaths,schemas,securitySchemes,examples. - Add
docs:validate+docs:buildtasks for the project's task runner. - Run
docs:build→ commit the HTML. - Add the ADR (
openapi-as-api-documentation-ssot) via/add-decision. - Add the CI
docs:validatestep (optional but recommended). - Remove legacy/duplicate docs sources.
- Update
CLAUDE.md/ editor rules to point at the SSOT.
{App}, {ORG}, {APP}, {host}, the auth env var names (e.g. NEXUS_API_KEY,
WMS_WEBHOOK_TOKEN), the list of public paths, and the OpenAPI tags (e.g. Inventory,
Webhooks — WMS).
{ "scripts": { "docs:validate": "redocly lint docs/openapi.yml", "docs:build": "redocly build-docs docs/openapi.yml -o public/api-docs.html" }, "devDependencies": { "@redocly/cli": "^1" } }