Skip to content

Generate OpenAPI spec from zod schemas + CI drift check - #12

Merged
stretchcloud merged 1 commit into
mainfrom
feature/openapi-from-zod
Jul 22, 2026
Merged

Generate OpenAPI spec from zod schemas + CI drift check#12
stretchcloud merged 1 commit into
mainfrom
feature/openapi-from-zod

Conversation

@stretchcloud

Copy link
Copy Markdown
Owner

Problem

swagger.yaml was hand-maintained with no tooling behind it, and had drifted badly:

  • documented 17 of 55 endpoints (~31%) — sessions, sites, agent, async tasks, search, parse, proxies, discover-apis, reader, metrics and more were entirely absent
  • described a batch path the code doesn't serve: /api/batch/scrape/{batchId}/result/{jobId} (actual: .../download/{jobId}) — anyone coding against it got a 404
  • omitted request options the API accepts: onlyMainContent, cssSchema, changeTracking, contacts, maxAge

Nothing could catch this: no swagger dependency, not served, not checked in CI.

Approach

Make the spec derive from the code rather than be written alongside it.

  • src/api/schemas/ — request zod schemas extracted from the route files into one pure module. The routes validate with these and the generator reads the same objects, so documented options can't diverge from what's accepted.
    It's deliberately pure (imports only zod): importing a route file pulls in Redis/BullMQ/browser-pool, which open handles at import time and would hang the generator.
  • src/api/openapi/ — builds the document via @asteasolutions/zod-to-openapi and writes swagger.yaml. Excluded from the tsc build so the devDependency never ships in dist/.
  • routes-inventory.ts — statically scans index.ts + routes/*.ts for the endpoints actually registered (parses source text; no imports, no side effects).
  • openapi.spec.ts — fails if a registered endpoint is undocumented, if the spec documents something not served, or if swagger.yaml is stale.
  • .github/workflows/ci.yml — build + test + npm run openapi:check.

Result

  • Coverage 17/55 → 55/55 endpoints (50 paths, 55 operations), organized under 11 tags
  • Stale batch path corrected; all previously-missing scrape options now documented
  • swagger.yaml carries a generated-file banner
npm run openapi:generate   # rewrite after changing a schema
npm run openapi:check      # verify in sync (CI runs this)

Verification

The guard was deliberately broken to confirm it isn't vacuous:

  • added a route → test failed naming it: GET /api/proxies/secret-undocumented
  • added a field to a zod schema → openapi:check failed telling you to regenerate
  • reverted → green again

Behaviour is unchanged — validation semantics preserved, including .passthrough() on the session action schema and the scrape options object.

tsc ✅ · eslint ✅ · 180/180 tests ✅ (174 existing + 6 new) · dist/ confirmed free of the generator

🤖 Generated with Claude Code

swagger.yaml was hand-maintained and had drifted badly: it documented 17 of
55 endpoints (~31%), described a batch path the code doesn't serve
(/result/{jobId} vs /download/{jobId}), and omitted request options the API
accepts (onlyMainContent, cssSchema, changeTracking, contacts, maxAge).

Make the spec derive from the code instead:

- src/api/schemas: request zod schemas extracted from the route files into a
  pure module (imports only zod). The routes now validate with these, and the
  generator reads the same objects, so validation and docs share one source.
  Kept pure deliberately - importing a route pulls in Redis/BullMQ/browser-pool,
  which open handles at import time and would hang the generator.
- src/api/openapi: builds the document via @asteasolutions/zod-to-openapi and
  writes swagger.yaml. Excluded from the tsc build so the devDependency never
  ships in dist.
- routes-inventory: statically scans index.ts + routes/*.ts for the endpoints
  actually registered (parses source, no imports).
- openapi.spec.ts: fails if any registered endpoint is undocumented, if the
  spec documents an endpoint that isn't served, or if swagger.yaml is stale.
- .github/workflows/ci.yml: build + test + `npm run openapi:check`.

Coverage is now 55/55 endpoints (50 paths, 55 operations). Verified the guard
is not vacuous: adding an undocumented route fails the test by name, and
editing a zod schema fails the sync check.

Behaviour unchanged - validation semantics preserved (incl. .passthrough()).
tsc, eslint and all 180 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@stretchcloud
stretchcloud merged commit a239e47 into main Jul 22, 2026
1 check passed
@stretchcloud
stretchcloud deleted the feature/openapi-from-zod branch July 22, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant