Skip to content

vladkens/apigen-ts

Repository files navigation

apigen-ts

version size downloads license donate

apigen-ts logo

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 npx in 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/cats
  • new 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

Development