Packages

api-contract

The executable definition of the HTTP API, kept outside every implementation of it.

statshub-api-contract is how the Go port is proven faithful rather than merely finished. It records golden responses from legacy and replays them against api.

The rule it exists to enforce

A test that imports the implementation dies with the implementation.

A test calling TypeScript functions in legacy is deleted along with the code it tested, the moment that route becomes Go. A test constructing a chi.Mux cannot check that Go matches what legacy actually served. Only a test that speaks HTTP to both survives the port and can compare them.

Running it

bun run contract:record    # capture goldens from legacy
bun run contract:verify    # replay them against Go
bun run contract:compare   # where cases and the ledger disagree

Against the parity stack, which brings both implementations up on one database:

bun run parity:up
bun run parity:record
bun run parity:verify

The parity stack covers what that compose file actually starts.

compare reads the port ledger

contract compare parses apps/statshub-api/docs/go-port.md for the routes claimed as ported. The path is pinned in src/cli.ts, so moving that file breaks the command — and the parser matches the Ported heading, so renaming it breaks it quietly rather than loudly.

Shape rules

src/shape.ts encodes the serialisation decisions clients depend on: numeric columns serialising as JSON strings, nullable columns staying present rather than being omitted. src/shape.test.ts checks them.

Read those before changing how anything serialises. A shape change that passes Go's own tests can still break every client — which is the whole reason this package is not inside either implementation.

When the port is meant to differ

Some differences are decisions, not regressions. /api/incident/{id} is the live example: a non-uuid id reaches a uuid column, Postgres raises 22P02, and both implementations answer 500 — but legacy serialised the driver's error object into the response, so the client got back the SQL text, the bound parameters and the server's source file and line. httpx.Fail sends error as {} and logs the rest.

expect.divergence records that, keyed by path, with the reason as the value:

"divergence": {
  "error": "Legacy serialised the postgres-js error object straight into the response, leaking the SQL text and the bound parameters. httpx.Fail sends error as {} and logs the real error."
}

Shape and body findings at or under a declared path stop failing the case. Nothing else can be declared — a status, header or transport difference is how a port breaks a client, and no reason makes one expected.

A declared path that produces no finding fails the case. That is what keeps this from being a mute button: if the two sides come to agree, the marker is hiding a regression that has not happened yet, and the suite says so rather than accepting the difference silently when someone reintroduces it.

GateWhat it proves
contract verifyGo's bytes match what legacy served
internal/api/v2_conformance_test.goThe OpenAPI document matches what the handlers write
bun run openapi:check in apps/statshub-apiThe checked-in document is not stale

On this page