- Status: Accepted
- Date: 2026-04-26
- Sub-project owner: Foundation
- Spec ref: Foundation §3
Backend and frontend live in the same repo (see ADR-0004) and must agree on every endpoint, payload shape, and error model. Hand-written DTOs on both sides drift quickly; tools like SignalR/gRPC are not what BRD §6.5 expects.
We need a contract source that is generated from one side, consumed by the other, and where drift fails CI loud and early.
- Source of truth: OpenAPI 3 specs in
contracts/—internal-api.yamlandexternal-api.yaml. - Backend: Swashbuckle generates the OpenAPI on every build, written to
contracts/. CI fails if the committed file differs from the generated one. - Frontend:
@hey-api/openapi-tsregenerates the typedapi-clientlib infrontend/libs/api-client/from those YAMLs. - Drift gate:
scripts/check-contracts-clean.shruns in CI; non-zero exit on any difference between generated and committed contracts (or generated and committed client code).
- Backend types and frontend types share one definition — diffing the YAML is the canonical way to review API changes.
- A breaking API change is impossible to commit silently — CI fails on the drift check.
- The api-client lib is regenerated, never hand-edited.
- Adds a generation step both sides must run before pushing.
- Swashbuckle annotations for non-trivial schemas (polymorphism, oneOf) take some care.
- Permission constraints can be projected into the OpenAPI as a custom extension (
x-permission) — frontend can use this for guard generation in a later sub-project (ADR-0013). - Treat OpenAPI changes as PR-worthy on their own; reviewers should diff the YAML.
- Rejected: drifts on every change; no automated detection.
- Rejected: NSwag's TS generation is opinionated in ways that don't fit the Nx + Angular Material + signals stack we use.
- Rejected: BRD spells out REST + JSON; gov consumers expect HTTP/JSON; gRPC/GraphQL increase ops complexity disproportionately.