Generate OpenAPI spec from zod schemas + CI drift check - #12
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
swagger.yamlwas hand-maintained with no tooling behind it, and had drifted badly:/api/batch/scrape/{batchId}/result/{jobId}(actual:.../download/{jobId}) — anyone coding against it got a 404onlyMainContent,cssSchema,changeTracking,contacts,maxAgeNothing 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-openapiand writesswagger.yaml. Excluded from thetscbuild so the devDependency never ships indist/.routes-inventory.ts— statically scansindex.ts+routes/*.tsfor 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 ifswagger.yamlis stale..github/workflows/ci.yml— build + test +npm run openapi:check.Result
swagger.yamlcarries a generated-file bannerVerification
The guard was deliberately broken to confirm it isn't vacuous:
GET /api/proxies/secret-undocumentedopenapi:checkfailed telling you to regenerateBehaviour 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