API contract
api/openapi.yaml, the contract tests that keep the server true to it, and the lint.
api/openapi.yaml is the contract of the HTTP API, in OpenAPI 3.1. Clients, such as the JavaScript client, are written against it, so the server must never drift from it.
What it covers
- Every route: health probes,
/_auth,/_adminand the generic document routes. - Every status an operation documents explicitly, plus a
defaultfor errors that can happen anywhere (404for an unknown realm,413,415,500,503). - Response bodies, including the error envelope and its closed list of
codevalues. - Response headers clients rely on:
ETag,Location,Retry-After,WWW-Authenticate,X-Request-ID, andCache-Control: no-storeon account data. - Documents as open objects. Their fields depend on each collection’s
schema.json, butidand_metaare described exactly.
How it’s enforced
Two checks keep the spec honest, and both run in CI.
Contract tests (internal/httpapi/contract_test.go) run the real HTTP handler and check every response against the spec:
- the operation is found from the router’s own route pattern, and the status must be listed on the operation;
defaultonly stands for404,413,415,500and503; - JSON bodies are validated against the spec’s schemas (OpenAPI 3.1 schemas are JSON Schema 2020-12), with formats such as
date-timeasserted; - required headers must be present and match their schemas; operations without a body must not send one.
They also check coverage in both directions: every route of the server is in the spec, every operation in the spec is a route, and every explicitly documented status is produced by at least one test. A separate test feeds deliberately broken responses to the checker to prove it catches them.
Lint: make lint-api runs Redocly’s strict ruleset, so the spec is valid OpenAPI and warnings fail. Deliberate exceptions are listed in api/.redocly.lint-ignore.yaml with the reason.
Changing the API
Change the spec in the same commit as the server. If a handler starts returning a new status, a new field or a new header, the contract tests fail until the spec says so, and a newly documented status fails until a test produces it.
New API or config surface extends the generators too
Two generators read the same loaded config and the contract, so a feature that adds API or config surface isn’t done until they know it:
- the type generator (
internal/typegen, behindbackd types): new document fields, collection options that change what a client may write or query, function modes and context members, configuration, templates and roles, with a golden file ininternal/typegen/testdata; - the per-realm OpenAPI (
internal/openapigen, behindbackd openapi): new routes, or routes that exist only when a realm enables something, each with a test that the route is present when enabled and absent when not. CI lints the example realms’ documents and builds clients from them (make openapi-clients).
Both are derived from api/openapi.yaml: documenting the route there is the first step.