chowbea-axios
Pinned Inputs

Pinned Inputs

Commit two small JSON files as a lockfile for your API contract. Gitignore everything generated. Let CI do the updating.

On a full-stack team, the worst place for committed types to come from is whichever backend a dev happened to be running. Pinned inputs (v3) makes that structurally impossible: the repo commits the inputs — openapi.json and chowbea.bus.json — and gitignores every generated file. Think package-lock.json, but for your API contract.

The model

PathIn git?Written by
openapi.json (pinned spec)✅sync only
chowbea.bus.json (pinned manifest)✅sync only
api.config.toml✅you
api.config.local.toml❌ gitignoredeach dev, optional
src/api/_generated/**❌ gitignoredgenerate / fetch
src/api/_internal/**❌ gitignoredfetch

Your hand-editable client files (api.client.ts, instance, error helpers) stay committed exactly as before.

Every build — dev machine or CI — runs generate offline from the two pins. Checking out any commit reproduces the exact types it was reviewed against. No endpoint needs to be up; no PR build can be skewed by a mid-deploy backend.

Turning it on

Pinned mode is the presence of two config keys, not a flag:

api.config.toml
api_endpoint = "https://staging.example.com/openapi.json"  # stable endpoint — sync's source
spec_file    = "openapi.json"                              # pinned spec, committed

[bus]
endpoint = "https://staging.example.com/.well-known/chowbea.json"
file     = "chowbea.bus.json"                              # pinned manifest, committed

New project? init --pinned scaffolds all of it — config, gitignore entries, both CI workflows, and a first sync. Existing project? See Migration.

Three commands, three jobs

  • generate — offline. Pins → _generated/. What CI runs on every PR. Never touches the network.
  • fetch [--endpoint URL] — the dev loop. Pulls from a live backend (localhost, a tunnel, anywhere) into gitignored paths only. Never writes the pins, so nothing you generate locally can reach a commit.
  • sync — the only pin writer. Fetches from the committed stable endpoint, validates everything, writes the pins only when their content changed, regenerates. CI runs it when the backend deploys.

After a localhost fetch, a bare generate snaps _generated/ back to the pinned types. That's deliberate — pins always win for generate, no freshness guessing. Want live types again? Run fetch again.

Pointing your workspace anywhere

Ports differ. Tunnels differ per dev. Don't put your URL in the committed config — overlay it:

api.config.local.toml (gitignored)
api_endpoint = "https://my-tunnel.ngrok.app/openapi.json"

[bus]
endpoint = "https://my-tunnel.ngrok.app/.well-known/chowbea.json"

Field-level merge, local wins, nested tables merge per key — your [bus].endpoint override keeps the committed [bus].file. fetch, generate, and watch apply it (and log which keys are overridden). sync deliberately ignores it — pins come from the committed truth only, so a dev running sync by hand can never pin their tunnel's contract into git.

[fetch.headers] and [fetch.auth] overlay the same way, so a tunnel that needs its own auth works too. One rule: Basic Auth is refused over plain http:// to anything that isn't loopback — use an https tunnel.

A day in the life

  1. Backend + frontend running locally → chowbea-axios fetch (with your local overlay, zero flags). Editor sees your in-progress types.
  2. Push your branch → the commit contains app code only. PR CI regenerates from the pins and typechecks — offline, reproducible.
  3. Backend merges and deploys → its pipeline rings the doorbell → your repo gets a chowbea/sync PR with the contract diff (bus: + Grade, ~ Plan), typechecked against your whole app before anyone merges it.
  4. No backend change? No PR. The pins are byte-stable, so a no-op sync produces literally nothing.

Never hand-edit the pins. The manifest is hash-validated end to end — a tampered chowbea.bus.json fails generate on the spot, which is exactly what you want from a trust boundary.

Next steps

On this page