feat(api): OpenAPI 3.1 spec generation + /api/openapi.json + CI drift gate (#178) #193
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!193
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "178-openapi-spec-generation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Carol's API contract is now machine-readable. Two commits:
b62db24feat — the substrate. Pulls in@asteasolutions/zod-to-openapi, wires every route into a central registry atlib/api/openapi-routes.ts, mirrors each DTO interface as a zod schema for drift-detected responses, serves the spec atGET /api/openapi.json, and adds three npm scripts (openapi:generate,openapi:check,openapi:coverage) plus a new CI job that hard-fails on drift or unregistered routes.0182366chore — the initial generatedopenapi.json(4583 lines), split into its own commit so the substantive diff stays reviewable.52 (path, method) pairs registered across the 30 route files. The OpenAPI coverage script walks
app/api/**/route.tsand confirms every route is in the registry; CI fails if a new route ships without a registry entry.Decisions baked in
@asteasolutions/zod-to-openapi(v8.5.0) — full zod 4 support, mature, handles.transform()/.refine(), registry-based API. The one shape it can't introspect isz.custom<T>(used for the theme preference field); a spec-only string-typed mirror lives inlib/api/openapi-routes.tswhile the runtime validation inlib/dto/settings.tsstays unchanged.lib/api/openapi-routes.ts). One audit surface; the coverage script catches missing registrations.info.version: "1.0.0"hard-coded — the API contract version is independent of the service release version inpackage.json.servers[]in the spec — self-hosted instances vary; clients build URLs relative to wherever they fetched the spec.openapi.json) — drift gate needs a committed baseline.tsxas a devDep for running the TS scripts. Lightweight, zero-config.app/themes/preferences.tsholds the pure-logic types/predicates;app/themes/registry.tsre-exports them and keeps the CSS side-effect imports. Non-UI modules (DTOs, scripts) import frompreferences.tsso script-time loads don't pull CSS through tsx.Files of note
lib/api/openapi.ts— registry, generator, security schemes,zProblemDetails.lib/api/openapi-routes.ts— 52registerPath(...)calls. The contract.app/api/openapi.json/route.ts— public endpoint.scripts/openapi-{generate,check,coverage}.ts+ npm scripts..forgejo/workflows/pr.yml— newopenapiCI job.lib/dto/{note,profile,skill,education,job,user,settings,pat}.ts.Out of scope (per the approved plan)
/api/v2/URL versioning — per ADR-0027 +docs/api-conventions.md, today's API is v1 with no URL prefix./api/healthin the spec — ops endpoint outside the v1 contract.Test plan
npm run typecheckgreennpm run lintgreennpm run testgreen — 499 tests pass (up from 497, plus 2 new for the spec endpoint)npm run openapi:generatewritesopenapi.jsonnpm run openapi:checkexits 0 on a clean checkout; exits 1 after editing a schema without regeneratingnpm run openapi:coveragereports 52 (path, method) pairs registeredcurl -s .../api/openapi.json | jq .openapireturns"3.1.0"(verified via test endpoint)/api/openapi.jsonreachable without a session (allowlist update verified)Closes #178. Third ticket under epic #176 — sets up the spec that #182 (generated typed client) will consume.
Carol's API contract is now machine-readable. The spec is generated from the route handlers' zod schemas via @asteasolutions/zod-to-openapi, served at GET /api/openapi.json (public allowlist), and CI-gated against drift so a schema change can't merge without the spec catching up. - lib/api/openapi.ts: registry, generator, zProblemDetails mirroring lib/api/errors.ts, cookieAuth + bearerAuth security schemes, hard-coded info.version = 1.0.0 per the v1 contract. - lib/api/openapi-routes.ts: central registry — every (path, method) registered with tags, summary, request schema, response schemas keyed by status, security. 52 (path, method) pairs covered. - 8 DTO files gain mirror response schemas (zNoteDto, zUserDto, ...). The existing XxxDto interfaces become `type XxxDto = z.infer<...>`, same field shape, drift-detectable both ways. - app/api/openapi.json/route.ts: public endpoint serving the spec. Added to lib/auth/public-routes.ts allowlist. - scripts/openapi-{generate,check,coverage}.ts + npm scripts. openapi:check regenerates and diffs against the committed copy; openapi:coverage walks app/api/** and confirms every route is registered. - .forgejo/workflows/pr.yml: new `openapi` job runs both checks alongside typecheck/lint. Hard fail on drift. - app/themes/preferences.ts: pure-logic split-out of registry.ts (no CSS imports) so non-UI modules can import the theme types without dragging CSS into script-time loads. - docs/api-conventions.md: new "Spec generation" section. - CLAUDE.md "API contract is codified" mentions the drift gate. - README.md gains an "API" section. The initial committed openapi.json (4583 lines) lands in the next commit so the substantive diff stays reviewable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>📊 Test coverage
Patch coverage: 100.0% (896/896 added lines) ✅ (soft target ≥ 80%)
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Changed files in this PR (source only — tests excluded):
app/api/openapi.json/route.tsapp/themes/preferences.tsapp/themes/registry.tslib/api/openapi-routes.tslib/api/openapi.tslib/auth/public-routes.tslib/dto/education.tslib/dto/job.tslib/dto/note.tslib/dto/pat.tslib/dto/profile.tslib/dto/settings.tslib/dto/skill.tslib/dto/user.tsSoft thresholds per ADR-0019. Coverage is informational and does not block merge.
Trivy (container image)
Threshold:
high· Total findings: 121 · At/above threshold: 16.27.0, 7.28.0, 8.5.0