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 extractLoads 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.jsonRe-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
| Flag | Effect |
|---|---|
-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 |
--check | Validate 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-removed | With --diff, make removals a hard failure |
--watch | Re-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:
{
"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
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);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.jsonYou 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 snapshotFile 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 watchSave 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.