chowbea-axios
Type Bus

Extract & Serve

Generate the manifest at build time and serve it from your API in three lines.

Two jobs in your API repo: turn marked types into a manifest, then serve that manifest over HTTP.

Extract

chowbea-axios extract

Loads your real tsconfig.json, builds a TypeScript program, sweeps both marking channels, validates every rule, and writes chowbea.bus.json to the project root.

✓ done Wrote 14 type(s) to /repo/chowbea.bus.json

Re-running with nothing changed skips the write entirely:

✓ Type bus unchanged — 14 type(s).

The comparison is on the manifest's content hash, which covers the extracted types but not generatedAt — so identical source always produces an identical hash. That keeps --watch quiet and stops build tooling from seeing a rewritten file on every cycle. A missing or corrupt manifest is always rewritten.

If anything violates the rules, nothing is written and you get every violation at once:

src/bus.chowbea.ts:3   "GradeMap" references "Grade" (src/exams/models.ts:12) which is not
                       on the bus — add it to a .chowbea.ts barrel or mark it @chowbea-export.
src/bus.chowbea.ts:7   "LIMIT" is not a type alias, interface, or enum — only types ride the
                       bus in v1 (classes, consts, and functions are runtime values).
Type bus extraction failed with 2 error(s).

Flags

FlagEffect
-p, --project <path>Use a specific tsconfig.json instead of the nearest one
-o, --out <path>Write the manifest somewhere other than ./chowbea.bus.json
--checkValidate only — never writes. Non-zero exit on violations. The CI gate.
--diff <url|file>Compare against a baseline manifest and report added / changed / removed
--fail-on-removedWith --diff, make removals a hard failure
--watchRe-extract on change (300ms debounce). The dev loop.

extract needs your real tsconfig.json because it type-checks references to enforce the closed-world rule. If your build uses a non-default config (tsconfig.build.json), point at it with --project.

Wire it into the build

Deployed servers have no TypeScript source, so the manifest must be generated before deploy and shipped with the app:

package.json
{
  "scripts": {
    "prebuild": "chowbea-axios extract",
    "build": "nest build"
  }
}

Because the artifact is regenerated from the source being deployed, the served manifest can never drift from the deployed code. There's no committed file to fall out of sync — the whole class of bug is designed out.

Serve

chowbea-axios/api is deliberately tiny and framework-agnostic: no Nest module, no DI, no middleware stack. It reads a file and writes a response.

Mount the handler

src/main.ts (NestJS)
import { busHandler, DEFAULT_API_ROUTE } from 'chowbea-axios/api';

const app = await NestFactory.create(AppModule);
app.use(DEFAULT_API_ROUTE, busHandler());
await app.listen(3000);
Express
import express from 'express';
import { busHandler, DEFAULT_API_ROUTE } from 'chowbea-axios/api';

const app = express();
app.use(DEFAULT_API_ROUTE, busHandler());

DEFAULT_API_ROUTE is /.well-known/chowbea.json. Mount elsewhere if you prefer — the frontend just needs the URL.

Verify

curl -i http://localhost:3000/.well-known/chowbea.json

You want 200, content-type: application/json, and an ETag. A 503 with chowbea bus manifest unavailable — run chowbea-axios extract means exactly what it says.

Two modes

busHandler()                      // file mode (2.7.0+) — re-reads when the file changes
busHandler('path/to/manifest')    // file mode, explicit path
busHandler(readBusManifest())     // static mode — boot-time snapshot

File mode stats the manifest on each request and re-reads only when mtime or size changes. In production that's one statSync per request against a file that never changes; in development it means a fresh extract is served on the next request with no server restart — which matters because your framework's watcher (Nest, nodemon) watches .ts files, not chowbea.bus.json.

Static mode reads once at boot. Marginally cheaper, but a fresh extract won't be served until you restart. Use it only if you have a reason.

File mode is deliberately forgiving: if a read lands mid-write and the JSON is incomplete, it keeps serving the last-good manifest and retries on the next request rather than 500-ing. Only a manifest that has never loaded produces a 503.

Conditional requests

The handler sets ETag to the manifest hash and answers 304 Not Modified to a matching If-None-Match — including comma-separated lists, weak W/ validators, and *. The frontend sends the cached hash on every sync, so an unchanged bus costs a 304 and nothing else.

ESM and CommonJS

The ./api export supports both module systems from 2.6.0 — a plain static import works in a default (CommonJS) NestJS project:

import { busHandler, DEFAULT_API_ROUTE } from 'chowbea-axios/api';

On 2.5 and earlier the export was ESM-only. From CommonJS, load it dynamically inside your async bootstrap:

const { busHandler, DEFAULT_API_ROUTE } = await import('chowbea-axios/api');
app.use(DEFAULT_API_ROUTE, busHandler());

Dev loop

Two terminals, no restarts:

# API repo
chowbea-axios extract --watch

# frontend repo
chowbea-axios watch

Save a .chowbea.ts file → extract rewrites the manifest → the running server serves it (file mode) → the frontend's next sync emits fresh types. Seconds, no process restarts anywhere.

The watcher only reacts to project TypeScript sources. Build output (dist/, build/, .next/, coverage/), node_modules, and declaration files are ignored — otherwise a framework rebuild emitting dist/**/*.d.ts would re-extract on every compile. Combined with the unchanged-skip above, a busy dev loop produces no manifest churn at all.

On this page