npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bazo/openapi-client-generator

v3.1.80

Published

Generate type-safe API clients from an OpenAPI / Swagger JSON spec. One CLI, seven output targets:

Readme

@bazo/openapi-client-generator

Generate type-safe API clients from an OpenAPI / Swagger JSON spec. One CLI, seven output targets:

| Mode | Output | Stack | |------|--------|-------| | full (default) | .ts | React Query hooks + Wretch HTTP client + Zod validation | | playwright | .ts | Class wrapping Playwright's APIRequestContext for E2E tests + Zod validation | | backend | .ts | Server-side class over the platform fetch + Zod validation — Node ≥ 18, Bun, Deno; no React Query, no React | | swift | .swift | Codable structs + async/await client (Foundation only) | | kotlin | .kt | @Serializable data classes + Ktor-based suspend client | | php | directory of .php | PSR-4 classes + backed enums + Symfony HttpClient/Serializer client | | zod | <name>.ts | Only the component schemas as Zod 4 schemas plus z.infer types, in dependency order; no client. --schemas narrows it to chosen schemas and what they reference | | php-legacy | README.md + src/**.php | PHP 7.3/7.4 client: no Composer, no dependencies, ext-curl transport, one class per file, exceptions, OAuth2 client_credentials authenticator | | go | directory of .go | Structs + typed enums/unions + net/http client with typed per-operation errors (stdlib only) |

The TypeScript modes emit Zod 4 validators (z.email(), z.iso.datetime(), z.int(), z.strictObject(), getter-based recursion), so they need zod >= 4.

Schemas are normalized, sorted by dependency, and emitted with full types; unused declarations are stripped from TypeScript output automatically.

Input may be OpenAPI 3.0, 3.1, 3.2 or Swagger 2.0 — a 2.0 document is lifted to 3.0 first. From 3.2, the QUERY method and additionalOperations (any other method token) are generated as body-carrying calls in every mode, and an in: querystring parameter with an object schema is expanded into ordinary query parameters. Recursive schemas (a schema that refers to itself, or a cycle through several) are supported in every mode. The test suite generates every mode from Forgejo's published API (src/__tests__/fixtures/forgejo.json) as a real-world gate.

Installation

npm install -D @bazo/openapi-client-generator
# or: bun add -d @bazo/openapi-client-generator

You can also run it without installing:

npx @bazo/openapi-client-generator api.json src/api -n MyClient

Usage

openapi-client-generator <spec.json> <output-dir> -n <ClientName> [-m <mode>]

| Option | Description | |--------|-------------| | <spec.json> | Path to the OpenAPI/Swagger spec, JSON or YAML (positional, required) | | <output-dir> | Directory to write the generated file into (positional, required) | | -n, --name | Client/class name — must be a valid identifier (required) | | -m, --mode | full | playwright | backend | swift | kotlin | php | php-legacy | go | zod (default: full) | | --namespace | php, php-legacy: root PHP namespace (default: --name). swift: wrap the whole client in enum <ns> so no generated type lands at module scope | | --php-version | Target PHP version. php mode: 8.0–8.5 (default: 8.1). php-legacy mode: 7.3 or 7.4 (default: 7.3; 7.4 only adds typed properties) | | --token-scope | php-legacy only: the scope Auth\ClientCredentials requests. Normally taken from the spec's OAuth2 clientCredentials security scheme (its tokenUrl and first scope); without one, the authenticator is generated for the operation whose path ends in /token and uses this scope (none if omitted). An explicit value wins over the scheme | | --schemas | zod mode only: comma-separated component schema names; the output holds those plus every schema they reference (default: all). Unknown names fail. Ignored with a warning in other modes | | --package | Go package name, go mode only (default: --name lowercased) | | --lenient-enums | go mode only: decode enum values the spec does not declare instead of failing the response (the raw value is kept and round-trips; Valid() still reports false). Default: strict | | --operations | Generate only these operations: comma-separated operationIds (the spec's own, or the one the generator derives for an operation without one) or "METHOD /path". Repeatable. Components no selected operation reaches are dropped, in every mode. An unknown selector is an error | | --overlay | Apply an OpenAPI Overlay 1.0 file (JSON or YAML) to the spec before generating. Repeatable; applied in order | | -v, --version | Print version | | -h, --help | Print help |

The output file is named after --name with the extension chosen by the mode (MyClient.ts, ApiClient.swift, ApiClient.kt). The php mode instead writes a directory named after --name containing one PSR-4 file per class, the php-legacy mode writes README.md and src/ straight into the output directory, and the go mode a directory named after --name holding one Go package.

# React Query client
openapi-client-generator api.json src/api -n MyClient

# Playwright E2E client
openapi-client-generator api.json tests/api -n TestApi -m playwright

# Server-side (Node/Bun/Deno) client — BFFs, services, SDK packages
openapi-client-generator api.json src/api -n ApiClient -m backend

# Swift client
openapi-client-generator api.json Sources/Api -n ApiClient -m swift

# Kotlin client
openapi-client-generator api.json src/main/kotlin -n ApiClient -m kotlin

# PHP client
openapi-client-generator api.json src/Api -n ApiClient -m php --namespace "App\\Api" --php-version 8.2

# Go client
openapi-client-generator api.json internal -n PetStore -m go --package petstore

Vendor specs: --operations and --overlay

A vendor API often has hundreds of endpoints of which you call five, and a spec with gaps you cannot fix upstream. Generate only what you use, and patch the spec with an overlay instead of editing a copy of it:

openapi-client-generator brevo.yaml src/brevo -n BrevoClient -m backend \
  --operations sendTransacSms,getAccount,"DELETE /contacts/{identifier}" \
  --overlay brevo.overlay.yaml
# brevo.overlay.yaml — OpenAPI Overlay 1.0: JSONPath (RFC 9535) targets, applied in order
overlay: 1.0.0
info: { title: Brevo spec fixes, version: 1.0.0 }
actions:
  - description: Statuses the spec does not list (401, 403, 5xx) keep their error body
    target: $.paths.*.*.responses
    update:
      default: { $ref: "#/components/responses/errorModel" }
  - description: The error `code` enum is closed; Brevo adds codes without notice
    target: $.components.schemas.errorModel.properties.code.enum
    remove: true

An overlay applies to the document as written — before a Swagger 2.0 spec is converted (so it targets definitions, not components) and before $refs are resolved (so it can add or replace them). update merges into each matched object (nested objects merge, anything else — arrays included — is replaced) or appends to a matched array; remove: true deletes the match. An action that matches nothing is reported as a warning: it is usually a typo, or a vendor spec that moved.

A handy package.json script:

{
  "scripts": {
    "gen:api": "openapi-client-generator ./openapi.json ./src/api -n MyClient"
  }
}

Generated client examples

full — React Query

Peer dependencies: zod, wretch, @tanstack/react-query.

import { QueryClient } from "@tanstack/react-query";
import { MyClient } from "./api/MyClient";

const queryClient = new QueryClient();
const api = new MyClient("https://api.example.com", queryClient);

function TaskList() {
  const { data, isLoading } = api.listTasks.useQuery({ page: 1, limit: 10 });
  const create = api.createTask.useMutation();
  // ...
}

Constructor: new MyClient(baseUrl, queryClient, options?, onHttpConfigure?). Each operation exposes query/mutation (raw) and useQuery/useMutation (hooks); responses are typed as ApiResponse<T> and validated with Zod — every documented body strictly, successes and errors (exact, 4XX-style range or default) alike; a body that breaks its schema throws ResponseValidationError carrying the status. The Suspense fetch/queryOptions path throws ApiStatusError for a documented error status whose body validates (data/code from the exact → range → default lookup) and ResponseValidationError for one whose body does not.

playwright — E2E testing

Peer dependencies: zod, @playwright/test.

import { test, expect } from "@playwright/test";
import { TestApi } from "./api/TestApi";

test("list tasks returns 200", async ({ request }) => {
  const api = new TestApi(request);
  const result = await api.listTasks({ page: 1, limit: 10 });
  expect(result.status).toBe(200);
  expect(result.data.items.length).toBeGreaterThan(0);
});

Constructor takes Playwright's APIRequestContext (the { request } fixture). Operations are async methods returning a Zod-validated ApiResponse<T>; like backend, every documented body — success or error, exact, 4XX range or default — is validated strictly, and a mismatch throws ResponseValidationError carrying the status.

backend — Node / Bun / Deno

Peer dependency: zod only. HTTP goes through the platform fetch (Node ≥ 18, Bun, Deno) — no wretch, no TanStack Query, no React. Use it for server-side code: BFFs, services, and SDK packages that wrap an API; use full for React/React Native apps.

import { ApiClient } from "./api/ApiClient";

const api = new ApiClient("https://api.example.com", {
  // Called per request, so a refreshed token is always picked up (a plain HeadersInit works too).
  headers: async () => ({ Authorization: `Bearer ${await tokens.current()}` }),
  // Optional: any fetch-compatible function — a stub in tests, a custom agent, instrumentation.
  fetch: (input, init) => fetch(input, init),
});

const res = await api.getTask({ taskId: "42" }, { signal: AbortSignal.timeout(5_000) });
if (res.status === 200 && res.data) {
  console.log(res.data.title); // typed as the 200 schema
} else if (res.status === 404 && res.data) {
  console.warn(res.data.message); // typed as the 404 schema
}
  • Constructor: new ApiClient(baseUrl, options?). When the spec declares servers, baseUrl defaults to the first one (ApiClient.defaultBaseUrl; all of them are on ApiClient.servers), so new ApiClient() and new ApiClient(undefined, { headers }) work. A trailing / on baseUrl is ignored.
  • ApiClientOptions: fetch?: (input: string, init: RequestInit) => Promise<Response> (defaults to the global fetch, looked up per request) and headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>) (sent on every request).
  • Operations: async <operationId>(params, requestInit?). params holds path, query and header params plus body, and defaults to {} when nothing in it is required. requestInit is RequestOptions — { signal?: AbortSignal; headers?: HeadersInit }.
  • Responses: ApiResponse<T, S> — { data, status, headers: Headers, url }, literal status per exactly documented status. Every documented body is validated strictly; a mismatch throws ResponseValidationError carrying the status. That holds for successes and errors alike — an exact status (200, 401, 404, 500…), a 4XX-style range or default. A body that does not match its schema (the wrong shape, a key a strict schema does not declare, a proxy's HTML page on a documented status) is a contract violation: the call rejects with a ResponseValidationError that has status, headers, url, the raw body, and the ZodError (or, for a non-JSON body, the JSON SyntaxError) as cause; its message reads Response to GET /tasks/{taskId} (500) does not match the contract. A status is matched exact → range → default (Go mode's precedence); a status none of them documents resolves data: null — there is nothing to validate it against. A method never throws on an HTTP status as such — only on a network error / abort, invalid params (before the request), or that contract violation. The return type is one ApiResponse<T, S> per exact status plus a catch-all ApiResponse<Envelope, number> per range/default (and ApiResponse<null, number> without default), so check res.data after checking status.
  • Headers are layered: options.headers → the body's content type → the operation's header params → requestInit.headers, each winning over the one before. A multipart or urlencoded body always gets the content type fetch writes for it, even if a default Content-Type is set.
  • Encoding matches the other TS modes: path params percent-encoded; query arrays repeat the key (comma-joined for explode: false), objects are sent as JSON, undefined/null are left out; multipart via FormData, urlencoded via URLSearchParams, raw binary bodies as the Blob/File you pass (BinaryInput = Blob | File).

swift — iOS / macOS

No external dependencies (Foundation only). Add the .swift file to your Xcode project.

let api = ApiClient(baseURL: URL(string: "https://api.example.com")!)
let response = try await api.listTasks(page: 1, limit: 10)

Constructor: init(baseURL: URL, session: URLSession = .shared). Schemas are Codable structs; failures throw an APIError enum.

kotlin — Android / JVM

Uses Ktor + kotlinx.serialization.

val httpClient = HttpClient(CIO) {
    install(ContentNegotiation) { json() }
}
val api = ApiClient(client = httpClient, baseUrl = "https://api.example.com")
val response = api.listTasks(page = 1, limit = 10)

Constructor: ApiClient(client: HttpClient, baseUrl: String). Schemas are @Serializable data classes; operations are suspend functions returning ApiResponse<T> and throw ApiException on error.

// build.gradle.kts
plugins { kotlin("plugin.serialization") version "2.1.0" }
dependencies {
    implementation("io.ktor:ktor-client-core:3.1.0")
    implementation("io.ktor:ktor-client-cio:3.1.0")
    implementation("io.ktor:ktor-client-content-negotiation:3.1.0")
    implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}

php — Symfony HttpClient

Composer dependencies: symfony/http-client, symfony/serializer, symfony/property-info, symfony/property-access, phpdocumentor/reflection-docblock.

use App\Api\ApiClient;
use Symfony\Component\HttpClient\HttpClient;

$api = new ApiClient(HttpClient::create(), 'https://api.example.com');
$response = $api->listTasks(page: 1, limit: 10);
$response->data;   // typed model, hydrated via Symfony Serializer
$response->status; // 200

Constructor: new ApiClient(HttpClientInterface $client, string $baseUrl, ?SerializerInterface $serializer = null) (a sensible serializer is built by the generated SerializerFactory when omitted; the third parameter is dropped entirely for a spec whose operations never serialize or deserialize a typed body). Output is a PSR-4 directory — models under Model/, plus ApiResponse, ApiException (thrown on every non-2xx), and the client class. --php-version gates emitted features: backed enums + readonly need 8.1+, readonly class 8.2+, typed constants 8.3+, #[\Deprecated] 8.4+, #[\NoDiscard] + Uri\Rfc3986 URL building 8.5. Code generated for 8.0 uses serializer ^5.4|^6.x (AnnotationLoader); 8.1+ requires serializer >= 6.4.

php-legacy — PHP 7.3, no Composer

For consumers stuck on an old PHP (an information system on 7.3, say) that cannot install Symfony. Requirements: ext-curl and ext-json, nothing else. Unlike the php mode it writes into the output directory itself: README.md plus src/ with one class per file under --namespace (RobotLoader or any PSR-4 loader indexes src/).

openapi-client-generator spec/sync.openapi.json clients/php -n MedelySyncClient -m php-legacy \
  --namespace 'Medely\Client' --php-version 7.3
$client = new MedelySyncClient(
    new CurlTransport(),
    new ClientCredentials($clientId, $clientSecret, new FileTokenStore('/var/cache/app')),
    'https://api.example.com'
);
$doctor = $client->upsertDoctor('r-1', new Model\UpsertDoctorRequest('Jana', 'Nova')); // Model\DoctorResponse
  • Layout: <Client>.php (one public method per operation, returning the typed model, void for 204, the raw string for a byte download), Http/ (Transport, CurlTransport, Request, Response), Auth/ (Authenticator, ClientCredentials, TokenStore, MemoryTokenStore, FileTokenStore), Exception/ (MedelyException > ApiException with getStatusCode()/getErrorCode()/getBody()/getResponseHeaders(), TransportException, UnexpectedResponseException), Multipart/FilePart, Internal/Support, Model/*.
  • Auth\ClientCredentials is generated from the spec's OAuth2 clientCredentials security scheme: it POSTs the form with an explicit scope, caches the token (in a TokenStore) until expires_in minus 60 s, and retries once on a 401. An operation with security: [] (the token operation itself) is never authorized.
  • Models are immutable (private untyped properties with @var, 7.1-style constructor hints, getters only). fromArray() checks the required keys and throws UnexpectedResponseException; toArray() writes only declared fields, leaving out a null optional. Date-times are \DateTimeImmutable (DATE_ATOM). Enums are lenient: the property holds the raw string and the Model\<Enum> class is a constants holder with values()/isKnown(), so a value the API adds later cannot crash an old client.
  • Banned in the output (checked by a test): fn, match, ??=, enum, typed properties (7.3), named arguments, mixed as a type, ?->, attributes, union types. --php-version 7.4 only switches on typed properties.
  • Multipart bodies go through CURLFile (FilePart::fromString() writes a temp file, since CURLStringFile is 8.1+); an array field is sent as name[0], name[1].

go — standard library net/http

No dependencies beyond the standard library; Go 1.27+. The output is one package directory (client.go, operations.go, models.go, enums.go, unions.go, errors.go, runtime.go), already gofmt-formatted.

api, err := petstore.NewClient(
	petstore.WithBaseURL("https://api.example.com"),
	petstore.WithRequestEditor(func(ctx context.Context, req *http.Request) error {
		req.Header.Set("Authorization", "Bearer "+token)
		return nil
	}),
)
res, err := api.ListTasks(ctx, &petstore.ListTasksParams{Page: petstore.Ptr(1)})
res.Data       // TaskList
res.StatusCode // 200

_, err = api.GetTask(ctx, "missing")
var getErr *petstore.GetTaskError
if errors.As(err, &getErr) && getErr.Status404 != nil {
	fmt.Println(getErr.Status404.Message) // the documented 404 body, typed
}

Every operation is (ctx, path params…, required query/header params…, body, *<Op>Params) (*APIResponse[T], error). A non-2xx response is always that operation's own *<Op>Error, which has one typed field per documented error status and unwraps to the shared *APIError. Optional fields are pointers (Ptr helps), enums are typed constants that reject undeclared values (or, with --lenient-enums, keep them — check Valid() — so an API that grows an enum value, as App Store Connect does, does not break every response carrying it), and oneOf/anyOf unions are structs with one pointer per branch.

Agent skills

The package ships agent skills — Markdown playbooks for AI coding agents covering CLI usage (generate-client) and per-mode integration (integrate-{full,playwright,backend,swift,kotlin,php,php-legacy,go}-client, integrate-zod-schemas). Compatible tools discover them automatically once the package is installed.

Development

Requires Bun and Task.

bun install
task test        # run the vitest suite
task lint        # oxlint
task build       # bundle src/bin.ts -> dist/bin.js
task run         # generate a sample client from fixtures/api.json into out/

Run a single test file: bun vitest run src/__tests__/converter/primitives.test.ts

See CLAUDE.md for architecture details and the full task list. task run-pw, task run-backend, task run-swift, task run-kotlin, task run-php, task run-php-legacy, task run-go, and task run-zod generate sample clients for the other modes. Runtime integration tests (task test-runtime) require a Java/Gradle toolchain (Kotlin), a Swift toolchain (Swift), PHP >= 8.1 + Composer (PHP), Docker with the php:7.3-cli image (php-legacy: lint, PHPStan at phpVersion: 70300 and a runtime smoke test against a stub server), or Go >= 1.27 (Go) and are skipped otherwise.

License

No license has been declared for this package yet. Add a LICENSE file and a license field to package.json / package.json.dist before publishing publicly.