Skip to main content

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:

JobWhat it doesBlocking
lint-typecheck-testeslint (no --fix), tsc -p tsconfig.build.json, npm testyes
openapi-spec-up-to-dateregenerates the spec against a real Postgres, then git diff --exit-code openapi.jsonyes
e2e-testsbuilds, runs migrations, npm run test:e2e against a real DBno (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-date catches 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 on GET /api/config/capabilities alongside backendVersion. 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.