StatsHub Docs
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 the web API 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

pnpm contract:record   # capture goldens from legacy
pnpm contract:verify   # replay them against Go
pnpm contract:compare  # compare live legacy and Go responses

For a complete, request-deduplicated comparison of two running servers, use audit. It compares status, stable headers, raw bytes, decoded values, array order, and optionally database traces. It does not validate the live legacy response against an old golden; verify is the command for that separate question. Cache-Control directives remain exact, with a one-second tolerance only for a live s-maxage countdown crossing a clock tick between requests.

LEGACY_API_URL=http://127.0.0.1:4102 \
GO_API_URL=http://127.0.0.1:4100 \
pnpm --filter statshub-api-contract contract audit \
  --read-only --no-trace --max-requests=2118 \
  --report=.parity/read-only.json

The request ceiling is mandatory. --read-only is mandatory when the targets use a shared or production-like database; successful mutations are only safe with --isolated against the parity stack. Use --shard=1/4 through 4/4 when a complete response audit is too large for one process.

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

pnpm parity:up
pnpm parity:record
pnpm parity:verify

The parity stack covers what that compose file actually starts.

compare reads the port ledger

contract compare parses apps/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.

compare still requires each case to have a committed golden, which proves the case has been reviewed and keeps single-target verification reproducible. For the actual two-target comparison, the response legacy returns in that run becomes the in-memory reference for Go. Live row changes therefore do not look like port failures merely because a committed sample is old.

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/openapi_conformance_test.goThe OpenAPI document matches what the handlers write
pnpm openapi:check in apps/apiThe checked-in document is not stale

On this page