@n0safe/openapi-orpc-contracts
v1.3.1
Published
Generate tree-shakable oRPC contracts from OpenAPI 3.x specs (URL or file) via typed-openapi.
Readme
oapi — OpenAPI → oRPC contract generator
Production-ready CLI + programmatic API for generating tree-shakable oRPC contracts from any OpenAPI 3.x spec (URL or file), built with NestJS + nest-commander. Spec-agnostic: there is no bundled/default spec — you always pass --spec <url|file>.
Features
- Spec from URL or file —
--spec https://...or--spec ./my-spec.json(validated, cached). No default spec; never Cloudflare-aware. - In-memory zod generation — typed-openapi's programmatic API, no intermediate
.zod.tsfile - Per-endpoint contract files — only the schemas each endpoint uses (transitive closure, tree-shaken)
- Route-folder tree with
oc.router()— every directory has anindex.tsrouter carrying its path viaoc.prefix; each contract declarespath: "/" - Per-directory
common.ts— LCA placement: a schema lives in the lowest directory whose subtree contains every endpoint that needs it - Detailed input/output + status codes — real path params (
params),query,headers,body; every 2xx/3xx → status-typed output, every ≥400 →.errors()with oRPC standard codes - Runtime defect repair — circular schemas (
z.lazy),.extend()on non-objects (.and()),z.iso(zod/v4) - Fast IntelliSense — router type-alias memoization (
RootRouter,ZonesRouter, …) so the editor resolves each level as a cached named type - CLI + programmatic API — nest-commander commands and a typed
createGenerator()facade
Structure
src/
main.ts # CLI bootstrap (nest-commander CommandFactory)
app.module.ts # root NestJS module
index.ts # public programmatic API (createGenerator)
commands/ # nest-commander @Command classes
extract.command.ts # oapi extract <selector>
extract-many.command.ts # oapi extract-many [selectors...]
shrink.command.ts # oapi shrink
contracts/ # command/API option contracts (interfaces + mappers)
extract.contract.ts
extract-many.contract.ts
shrink.contract.ts
services/ # @Injectable() services (DI)
spec.service.ts # load + validate spec (URL/file, cached)
zod-generator.service.ts # in-memory zod/type generation + ctx building
extract.service.ts # orchestrate extractOne / extractMany + write files
shrink.service.ts # shrink schema subset
output.service.ts # filesystem writes + summaries
core/ # pure, framework-agnostic logic (no NestJS)
schema-file.ts # schema module indexing, closure, emit (z.lazy/.and repairs)
types-file.ts # type sidecar indexing + emit
generate.ts # typed-openapi programmatic generation
openapi.ts # spec helpers, JSON→zod compiler, operation compilation
router.ts # route tree, LCA common placement, router/endpoint emission
types/ # shared TypeScript interfaces
constants/ # defaults + HTTP/status constantsInstall & build
bun install
bun run buildbun run build produces a single self-contained monofile bin plus the
programmatic API bundle and type declarations:
| artifact | purpose |
| --- | --- |
| dist/main.js | monofile CLI (~9.5 MB): all real deps (NestJS, typed-openapi, …) bundled; only NestJS optional peers are external |
| dist/index.js | bundled programmatic API (createGenerator) |
| dist/**/*.d.ts | TypeScript declarations (tsc --emitDeclarationOnly) |
Only the NestJS optional peers (@nestjs/microservices, @nestjs/platform-express,
@nestjs/websockets, class-transformer, class-validator) are kept external —
our code never uses them, and inlining them would just bloat the monofile.
CLI
# dev (bun) — --spec is REQUIRED (URL or file path)
bun src/main.ts extract-many --spec https://example.com/openapi.json --all --out-dir out
bun src/main.ts extract-many --spec ./my-spec.json --tag Zone
bun src/main.ts extract-many --path-prefix /zones
bun src/main.ts extract-many zones-0-get "GET /zones"
bun src/main.ts extract "GET /zones/{zone_id}" --out endpoints/zone-details.ts
bun src/main.ts shrink --prefix zones_ --out slim.ts
# production (node) — the bin is a single self-contained file
node dist/main.js extract-many --spec ./my-spec.json --all --out-dir outOptions:
| command | flags |
| --- | --- |
| extract <selector> | -s, --spec, -o, --out, --validation, --no-tree-shake, --no-descriptions |
| extract-many [selectors...] | -s, --spec, -o, --out-dir, --all, --tag, --path-prefix, --id-prefix, --threshold, --flat, --no-common, --no-barrel, --validation, --no-tree-shake, --no-descriptions |
| shrink | -s, --spec, -o, --out, -k, --keep, -p, --prefix (repeatable), --validation, --no-tree-shake, --no-descriptions |
Programmatic API
import { createGenerator } from "@n0safe/openapi-orpc-contracts";
const gen = await createGenerator();
try {
const result = await gen.extract("https://example.com/openapi.json", "out", {
filters: { all: true },
});
console.log(result.stats.endpoints, "endpoints,", result.files.length, "files");
const one = await gen.extractOne("openapi.json", "zones-0-get", { out: "zone.ts" });
const slim = await gen.shrink("openapi.json", { prefixes: ["zones_"], out: "zones.ts" });
} finally {
await gen.appContext.close();
}Advanced users can also use the pure core/ functions directly (re-exported from the package root): buildSchemaIndex, buildTypeIndex, compileOperation, extractMany, extractOne, shrinkSchemas, …
Generated output shape
out/
index.ts export const router: RootRouter = oc.router({ accounts, zones, ... })
common.ts schemas shared across top-level routers (root LCA)
zones/
index.ts export const zones: ZonesRouter = oc.prefix("/zones").router({...})
common.ts schemas shared by 2+ endpoints under /zones
{zone_id}/
index.ts export const zone_id: ZoneIdRouter = oc.prefix("/{zone_id}").router({...})
common.ts
zones_0_get.ts contract for GET /zones/{zone_id} (path: "/")
purge_cache/
index.ts
common.ts
zone_purge.ts contract for POST /zones/{zone_id}/purge_cache (path: "/")Each leaf's baked route.path equals the exact full path:
import { router } from "./out";
router.zones.zone_id.purge_cache.post["~orpc"].route.path; // "/zones/{zone_id}/purge_cache/"Testing
The suite is vitest, split into unit, service, and e2e layers:
test/
fixtures/spec.json tiny spec exercising params, bodies, multi-status, enums, refs
unit/schema-file.spec.ts schema indexer, closures, z.lazy cycle repair, .extend() rewrite
unit/types-file.spec.ts type indexer, closures, emission
unit/openapi.spec.ts spec validation, name map, JSON→zod compiler, operation resolution/compilation
unit/router.spec.ts extractOne / extractMany (tree, flat, noCommon, noBarrel) / shrink
unit/spec.service.spec.ts SpecService: file + URL loading (fetch mocked), caching, validation
unit/extract.service.spec.ts ExtractService / ShrinkService with real DI + disk writes
e2e/programmatic.spec.ts createGenerator() booting the real NestJS context
e2e/cli.spec.ts compiled dist/main.js (help, extract, extract-many, shrink, error exits)Run:
bun run test # one-shot
bun run test:watch # watch modeThe e2e CLI tests build dist/ automatically if missing, and assert that
command errors exit non-zero (the CLI sets process.exitCode = 1 on
errors). Tests run in CI on every push/PR and before every npm publish.
Scripts
| command | action |
| --- | --- |
| bun run dev | run the CLI with bun (source) |
| bun run build | monofile bin + API bundle + tsc declarations → dist/ |
| bun run start | run the compiled CLI with node |
| bun run extract:all | all endpoints → out/ (bun dev mode, needs --spec) |
| bun run test | run the vitest suite |
| bun run test:watch | vitest watch mode |
| bun run typecheck | typecheck src/ |
| bun run typecheck:out | typecheck generated out/ |
