@openmirai/typeforge
v0.3.2
Published
Typeforge: headless OpenAPI to TypeScript codegen CLI and HTTPFetch runtime
Readme
@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 callers1. 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, coverageTest layout: test/README.md.
