@openmirai/typeforge
Headless OpenAPI / Swagger → TypeScript codegen. The CLI is typeforge. It reads a spec, writes typed route enums, request types, and HTTP caller functions, and never talks to a network.
- npm:
@openmirai/typeforge - GitHub: openmirai/typeforge
You own http.ts (the HTTPFetch adapter). Generated files import that adapter — they do not invent axios/fetch calls inline.
What it generates
For each source (a named API, e.g. atlas), under <apiRoot>/<source>/generated/:
| Output | Role |
|---|---|
types/**/*.d.ts |
Params, body, and response types per operation |
functions/**/*.ts |
Typed callers (getWidgets, …) |
routes.ts |
Routes string map + RouteTargets enum (name configurable) |
runtime.ts |
Re-exports HTTPFetch, httpFetch (if singleton), routes |
base.ts |
BaseResponse<T> when the spec uses a response envelope (or base.d.ts for split declaration output) |
Optional:
- TanStack Query — set
tanstackQuery: trueinsource.tsand add<apiRoot>/query-scope.ts. - Zod — wrap a schema with
createZodValidatorfrom@openmirai/typeforge/validation/zodand pass it asconfig.validateResponse.
Install
Requires Node.js 24+ (LTS). Use any package manager.
| Package manager | Install |
|---|---|
| npm | npm install --save-dev @openmirai/typeforge |
| pnpm | pnpm add -D @openmirai/typeforge |
| yarn | yarn add -D @openmirai/typeforge |
| bun | bun add -d @openmirai/typeforge |
Axios is an optional peer. Install axios only if you use --client axios.
Add a script so every package manager resolves the CLI from node_modules/.bin:
{
"scripts": {
"generate:types": "typeforge generate --all"
}
}
Then run npm run generate:types, pnpm run generate:types, yarn generate:types, or bun run generate:types.
CLI usage
Prefer the package.json script above. To invoke the binary directly:
| Command | npm | pnpm | yarn | bun |
|---|---|---|---|---|
| Init a source | npx typeforge init --source atlas --client axios |
pnpm exec typeforge init --source atlas --client axios |
yarn typeforge init --source atlas --client axios |
bunx typeforge init --source atlas --client axios |
| Generate one source | npx typeforge generate --source atlas |
pnpm exec typeforge generate --source atlas |
yarn typeforge generate --source atlas |
bunx typeforge generate --source atlas |
| Generate all sources | npx typeforge generate --all |
pnpm exec typeforge generate --all |
yarn typeforge generate --all |
bunx typeforge generate --all |
| Drift check (CI) | npx typeforge generate --all --check |
pnpm exec typeforge generate --all --check |
yarn typeforge generate --all --check |
bunx typeforge generate --all --check |
| Subcommand | Purpose |
|---|---|
init |
Scaffold http.ts, source.ts, known-types.ts |
generate |
Write generated files |
check |
Same as generate --check — exit 1 if output would change |
accept-base |
Update generated base.ts (base.d.ts for split declaration output) and patch models.ts BaseResponse |
--check and --accept-base cannot be combined. See docs/cli.md for the full command reference.
How the flow works
init → source.ts + http.ts → resolve spec → generate → typed callers
1. Init a source
typeforge init --source atlas --client axios
typeforge init --source orbit --client fetch --layout packages
--client is axios | fetch | custom. --layout is monolith (default, apiRoot = src/api) or packages (apiRoot = packages/utils/src/api).
Init creates (if missing):
typeforge.jsonwithapiRoot<apiRoot>/http.ts— yourHTTPFetchimplementation<apiRoot>/known-types.ts— optional schema → local type mapping<apiRoot>/<source>/source.ts— per-API config (type-safe template)<apiRoot>/<source>/generated/directory
Existing files are skipped.
2. Configure source.ts
Use defineSourceConfig for autocomplete and compile-time checks:
import { defineSourceConfig } from "@openmirai/typeforge";
export default defineSourceConfig({
spec: "./specs/acme.json",
functionsDir: "packages/utils/src/api/routes/atlas",
typesDir: "packages/types/src/api/atlas",
pathPrefix: "/api/acme/v3",
stripApiPrefix: true,
routeEnumName: "RouteTargets",
generationMode: "authoritative",
naming: "path",
ignorePaths: [],
maxRenderDepth: 50,
resolveMapKeyRefs: true,
tanstackQuery: false,
queryExtends: {
page: "page",
limit: "limit",
sortBy: "sortBy",
sortOrder: "sortOrder",
paginationTypeName: "OffsetLimitQuery",
paginationImportPath: "./pagination",
sortTypeName: "SortParams",
sortImportPath: "./pagination",
},
});
Plain export default { ... } still works; the CLI reads config fields from the file at generate time.
Re-exported types from the package root:
SourceConfig,QueryExtendsConfig,GenerationMode,NamingStrategydefineSourceConfig(config)— identity helper for typedsource.ts
| Field | Meaning |
|---|---|
spec |
Project-relative spec path (used when no --spec / env override) |
functionsDir |
Project-relative function output directory (defaults to the source's generated/functions) |
typesDir |
Project-relative type output directory (defaults to the source's generated/types; a generated base.d.ts is placed beside this directory when customized) |
pathPrefix |
Only generate operations under this prefix (e.g. /api/acme/v3) |
ignorePaths |
Extra paths to skip |
stripApiPrefix |
Strip a leading /api segment from route enum member names |
routeEnumName |
Enum name (default RouteTargets) |
generationMode |
authoritative (overwrite routes) or merge (keep extra enum members) |
naming |
path or operationId for function names |
queryExtends |
Fold page/limit/sort query params into shared pagination types |
tanstackQuery |
Emit Query helpers when query-scope.ts exists |
importBase |
Force import prefix for generated function files (overrides tsconfig aliases) |
maxRenderDepth / resolveMapKeyRefs |
Schema renderer limits |
unwrapResponseData |
Emit an envelope's data schema as the operation response type when the project's HTTPFetch already unwraps envelopes |
3. Spec resolution (first match wins)
--spec <path>- Env
OPENAPI_SPEC_<KEY>— source key uppercased, hyphens → underscores specin that source’ssource.tstypeforge.local.json(gitignored) map of{ "<source>": "<path>" }- Committed snapshot
<apiRoot>/<source>/spec.json
4. Envelope modes
Inferred from success response schemas. Details: docs/envelope.md.
| Mode | When | Types |
|---|---|---|
| shared | One envelope shape (data / success / message) |
BaseResponse<Unwrapped> |
| raw | No shared envelope | Spec schema as-is |
| mixed | Some ops have data, others do not |
Unwrap per operation when data exists |
Set unwrapResponseData: true when the project's injected HTTPFetch
normalizes successful envelope bodies before returning { data }. Every
operation whose success schema is recognized as an API envelope then receives
its data payload type. Data-only objects and business payloads that also
contain success remain raw. Metadata-only envelopes without a data field
receive the null type,
matching clients that normalize an omitted payload to null.
The default remains envelope-preserving and is compatible with the bundled
Axios and Fetch adapters.
5. HTTPFetch (http.ts)
Adapters implement HTTPFetch from @openmirai/typeforge/http (or the axios/fetch adapter packages). Methods return Promise<{ data: TResponse }>.
- If
http.tsexportshttpFetch, generated functions call that singleton. - Otherwise they take
props.http: HTTPFetch(injected).
6. Path-alias aware imports
Generated function files import types and runtime, and generated response
types import the generated base declaration (base.ts in monolith output, or
base.d.ts for split declaration output), using:
importBaseinsource.ts, if set- Else
compilerOptions.pathsfrom the nearest ancestortsconfig.jsonwith path aliases, starting at the correspondingfunctionsDirortypesDir - Else relative paths (
../../runtime)
Where files go
typeforge.json:
{ "apiRoot": "packages/utils/src/api" }
You can also set "typeforge": { "apiRoot": "..." } in package.json. The JSON file wins.
Monolith (--layout monolith, default):
src/api/http.ts
src/api/known-types.ts
src/api/models.ts # optional BaseResponse drift check
src/api/query-scope.ts # optional TanStack
src/api/atlas/source.ts
src/api/atlas/spec.json # optional snapshot
src/api/atlas/generated/…
Packages layout (--layout packages): typical placement is packages/utils/src/api/<source>/.
Set functionsDir and typesDir when callers and declarations belong in
different packages. Relative imports continue to work without aliases; when a
nearby tsconfig.json maps both output roots, deep generated imports use those
aliases automatically.
Zod (optional)
import { createZodValidator } from "@openmirai/typeforge/validation/zod";
import { widgetListSchema } from "./widget-list";
await getWidgets({
params: { page: 1, limit: 20 },
config: { validateResponse: createZodValidator(widgetListSchema) },
});
Releasing
Publishes go through npm Trusted Publishing (GitHub Actions OIDC). Do not npm publish from a laptop.
| Value | |
|---|---|
| npm package | @openmirai/typeforge |
| GitHub repo | openmirai/typeforge |
| Workflow | .github/workflows/publish.yml |
| Tag | v* (e.g. v0.1.3) |
Develop this repo
This repository uses pnpm for its own CI. Consumers are not required to use pnpm.
pnpm install
pnpm verify # format, lint, typecheck, build, coverage
Test layout: test/README.md.