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.
npm i apigen-ts --save-dev# 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.
import { ApiClient } from "./api-client"
const api = new ApiClient({
baseUrl: "https://example.com/api",
headers: { Authorization: "secret-token" },
})// 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" })const { token } = await api.auth.login({ username, password })
api.Config.headers = { Authorization: token }
await api.protectedRoute.get() // authenticatednpx apigen-ts ./openapi.json ./api-client.ts --parse-datesconst pet = await api.pet.getPetById(1)
const createdAt: Date = pet.createdAt // parsed from format=date-time stringPass --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",
}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'# 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 internalWhen both flags are set, --exclude-tags wins.
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-optionsconst controller = new AbortController()
await api.pet.getPetById(1, { signal: controller.signal })
// cancel the request
controller.abort()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" }
}
}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/1import { 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()
},
})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)| 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 |