Turn your OpenAPI spec into a typed TypeScript client with one command.
- One file. Outputs a single
api-client.ts— no scattered modules, no runtime deps in generated code. - Fully typed. Every method returns the exact response type from your schema. No casting, no
any. - Pure Node.js. No Java, no Docker. Works with
npxin any project. - Fetch-based. Uses native
fetch. Override it with your own function for auth, retries, or logging. - All OpenAPI versions. Supports v2 (Swagger), v3, and v3.1 — auto-upgrades v2 on the fly.
- Extras built in. Automatic date parsing, string literal unions instead of enums, Prettier formatting.
- Filterable. Include or exclude endpoints by path regex or tag — essential for large schemas.
Unlike openapi-typescript, it generates a ready-to-call client — not just types. Unlike openapi-generator-cli, it's pure Node.js with zero Java dependency. Unlike openapi-typescript-codegen, it outputs a single file.
Install
npm i apigen-ts --save-dev
Usage
1. Generate
# From a local file npx apigen-ts ./openapi.json ./api-client.ts # From a URL npx apigen-ts https://petstore3.swagger.io/api/v3/openapi.json ./api-client.ts # From a protected URL npx apigen-ts https://secret-api.example.com ./api-client.ts -H "x-api-key: secret-key"
Run npx apigen-ts --help for all options. See generated examples.
2. Import
import { ApiClient } from "./api-client" const api = new ApiClient({ baseUrl: "https://example.com/api", headers: { Authorization: "secret-token" }, })
3. Use
// GET /pet/{petId} await api.pet.getPetById(1) // → Pet // GET /pet/findByStatus?status=sold await api.pet.findPetsByStatus({ status: "sold" }) // → Pet[] // PUT /user/{username} — second arg is typed request body await api.user.updateUser("username", { firstName: "John" })
Advanced
Login flow
const { token } = await api.auth.login({ username, password }) api.Config.headers = { Authorization: token } await api.protectedRoute.get() // authenticated
Automatic date parsing
npx apigen-ts ./openapi.json ./api-client.ts --parse-dates
const pet = await api.pet.getPetById(1) const createdAt: Date = pet.createdAt // parsed from format=date-time string
String unions instead of enums
Pass --inline-enums to generate string literal unions — useful for Node.js type stripping:
npx apigen-ts ./openapi.json ./api-client.ts --inline-enums
// Generated: type MyEnum = "OptionA" | "OptionB" // Instead of: enum MyEnum { OptionA = "OptionA", OptionB = "OptionB", }
Filter by path
Include only the endpoints you need — useful with large schemas (e.g. Cloudflare's 8 MB monolith):
npx apigen-ts ./openapi.json ./api-client.ts --filter-paths '^/accounts'Filter by tag
# include only endpoints tagged "pets" or "store" npx apigen-ts ./openapi.json ./api-client.ts --include-tags pets,store # exclude endpoints tagged "internal" npx apigen-ts ./openapi.json ./api-client.ts --exclude-tags internal
When both flags are set, --exclude-tags wins.
AbortController / cancellation
Pass --fetch-options to add an optional last argument to every generated method, accepting any RequestInit field (including signal):
npx apigen-ts ./openapi.json ./api-client.ts --fetch-options
const controller = new AbortController() await api.pet.getPetById(1, { signal: controller.signal }) // cancel the request controller.abort()
Error handling
Non-2xx responses throw — the caught value is the parsed response body:
try { await api.pet.getPetById(404) } catch (e) { console.log(e) // awaited response.json() }
Override ParseError to control the shape:
class MyClient extends ApiClient { async ParseError(rep: Response) { return { code: "API_ERROR" } } }
Base URL resolving
By default uses the URL constructor: new URL(path, baseUrl). Notable behavior:
new URL("/v2/cats", "https://example.com/v1/")→https://example.com/v2/catsnew URL("v2/cats", "https://example.com/v1/")→https://example.com/v1/v2/cats
Override PrepareFetchUrl to change this (see #2):
class MyClient extends ApiClient { PrepareFetchUrl(path: string) { return new URL(`${this.Config.baseUrl}/${path}`.replace(/\/{2,}/g, "/")) } } const api = new MyClient({ baseUrl: "https://example.com/v1" }) await api.pet.getPetById(1) // → https://example.com/v1/pet/1
Node.js API
import { apigen } from "apigen-ts" await apigen({ source: "https://petstore3.swagger.io/api/v3/openapi.json", output: "./api-client.ts", // optional: name: "MyApiClient", // default: "ApiClient" parseDates: true, // default: false inlineEnums: false, // default: false fetchOptions: true, // default: false filterPaths: /^\/pets/, // only include paths matching regex includeTags: ["pets", "store"], // only include these tags excludeTags: ["internal"], // exclude these tags (wins over includeTags) headers: { "x-api-key": "secret-key" }, resolveName(ctx, op, proposal) { // proposal is [namespace, methodName] if (proposal[0] === "users") return // use default const [a, b] = op.name.split("/").slice(3, 5) // /api/v1/store/items/search return [a, `${op.method}_${b}`] // → api.store.get_items() }, })
Usage with FastAPI
By default, FastAPI generates verbose operationIds. Fix with a custom resolver:
from fastapi import FastAPI from fastapi.routing import APIRoute app = FastAPI() # add your routes here def update_operation_ids(app: FastAPI) -> None: for route in app.routes: if isinstance(route, APIRoute): ns = route.tags[0] if route.tags else "general" route.operation_id = f"{ns}_{route.name}".lower() # call after all routes are added update_operation_ids(app)
Alternatives
| Package | Issue |
|---|---|
| openapi-typescript-codegen | No single-file output (#1263) |
| openapi-typescript | Low-level types only — no callable client, no named type exports |
| openapi-generator-cli | Wraps a Java library |
| swagger-typescript-api | Complex config, breaking API changes between versions |