API Contract & CI
openapi.json is a committed artifact
The spec lives at the repo root, regenerated by a script — never hand-edited:
npm run generate:openapi # = nest build && node dist/scripts/generate-openapi.js
The generator boots the real AppModule and calls the same
buildSwaggerDocument() (src/swagger.config.ts) the live / docs UI
uses, so the file can't drift from what the running API would show. It
needs a reachable database — NestFactory.create() connects TypeORM
eagerly — but reads no data.
It must run against dist/, not ts-node. The @nestjs/swagger CLI
plugin (enabled in nest-cli.json) that infers response types from
TypeScript return types is a nest build transform. Run the generator via
ts-node and it silently produces the old, mostly-untyped spec.
CI enforces it
.github/workflows/ci.yml runs on every PR to develop / main:
| Job | What it does | Blocking |
|---|---|---|
lint-typecheck-test | eslint (no --fix), tsc -p tsconfig.build.json, npm test | yes |
openapi-spec-up-to-date | regenerates the spec against a real Postgres, then git diff --exit-code openapi.json | yes |
e2e-tests | builds, runs migrations, npm run test:e2e against a real DB | no (continue-on-error) |
If you change a response shape and forget to regenerate,
openapi-spec-up-to-date fails. The fix is always the same:
npm run generate:openapi
git add openapi.json && git commit
Generic responses need a decorator
@nestjs/swagger can't resolve a schema for Promise<PaginatedResponse<T>>
— TypeScript erases T. Paginated endpoints use
@ApiPaginatedResponse(Model)
(src/app/common/decorators/api-paginated-response.decorator.ts), which
builds the allOf / $ref schema once. Apply it to every paginated
controller method rather than hand-writing merged schemas.
Coverage is intentionally not 100%
Delete / deactivate / toggle actions, one file download, and GET /config's genuinely dynamic dictionary are left untyped on purpose —
forcing a fake shape onto them would be confidently-wrong documentation.
The current untyped list is maintained in the backend README. Codegen
gives unknown / any for those; that's expected.
apiContractVersion vs. the spec check
Two different signals, both needed:
openapi-spec-up-to-datecatches any drift between code and the committed spec, breaking or not, at build time.API_CONTRACT_VERSION(src/app/config/api-contract-version.ts) is a hand-maintained integer, exposed onGET /api/config/capabilitiesalongsidebackendVersion. Bump it only on a breaking change — removed/renamed field, changed type, removed endpoint. The frontend compares it against what it was built for and shows a dismissible mismatch banner (warn-only, never blocks). See ADR-002 in Architecture Decisions.
Don't bump API_CONTRACT_VERSION for additive changes — a new optional
field or a new endpoint isn't breaking.