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

opencodegen

v0.3.1

Published

Generate typed API clients from OpenAPI specifications

Downloads

466

Readme

OpenCodegen

Generate typed TypeScript API clients from OpenAPI 3.0 / 3.1 specifications.

OpenCodegen is opinionated. The output is a single fixed shape — interface per schema, class per OpenAPI tag inheriting from a shared BaseClient, native fetch, no barrel index.ts, constObject enums by default. The config options listed below are the only knobs. If you need a different layout (Axios, hooks, alternate enum styles, split request/response types), this isn't the tool.

Install

npm install --save-dev opencodegen
# or
bun add -d opencodegen

Quick start

Create opencodegen.config.ts in your project root:

import { defineConfig } from 'opencodegen';

export default defineConfig({
  source: 'https://api.example.com/openapi.json',
  target: './src/generated',
  codegen: {
    dateType: 'string',
    enumType: 'constObject',
    propertyNameStyle: 'original',
    nullableType: 'null',
  },
});

Then run:

npx opencodegen

Run npx opencodegen --help for flag details (custom config path, verbose output).

Generated output

src/generated/
├── base.ts           # BaseClient, ApiConfig, ApiError
├── types.ts          # All interfaces
├── pets-client.ts    # PetsClient extends BaseClient
└── store-client.ts   # StoreClient extends BaseClient

Use it:

import { PetsClient } from './generated/pets-client.js';
import { ApiError } from './generated/base.js';

const client = new PetsClient({ baseUrl: 'https://api.example.com' });

try {
  const pet = await client.getPet(123);
} catch (err) {
  if (err instanceof ApiError) {
    // err.body is typed as ApiErrorBody — the union of declared 4xx/5xx response schemas
    console.error(err.status, err.body?.message);
  }
}

Authentication

BaseClient accepts a headers option that's resolved on every request. It can be a static object, a sync function, or an async function — pick whichever fits your token source.

Static — API key or pre-issued bearer

const client = new PetsClient({
  baseUrl: 'https://api.example.com',
  headers: { Authorization: `Bearer ${API_TOKEN}` },
});

Dynamic — sync

For tokens you can read synchronously (env var, in-memory store, redux selector):

const client = new PetsClient({
  baseUrl: 'https://api.example.com',
  headers: () => ({
    Authorization: `Bearer ${tokenStore.current()}`,
  }),
});

Dynamic — async (MSAL, Auth0, refresh flows)

The function runs on every request. Rely on your auth library's own token cache so you're not hitting the network each call:

const client = new PetsClient({
  baseUrl: 'https://api.example.com',
  headers: async () => {
    // acquireTokenSilent caches internally and only refreshes near expiry
    const result = await msalInstance.acquireTokenSilent({ scopes });
    return { Authorization: `Bearer ${result.accessToken}` };
  },
});

Per-request overrides

Every generated method accepts a final requestOptions argument. Per-request headers are merged on top of config headers (last-write-wins), so you can override or add headers for a single call without rebuilding the client:

await client.getPet(123, {
  headers: { 'X-Correlation-ID': crypto.randomUUID() },
});

Cancellation

The same requestOptions shape carries an AbortSignal:

const ctrl = new AbortController();
const promise = client.listPets({ signal: ctrl.signal });
ctrl.abort(); // rejects `promise` with an AbortError

Sharing config across clients

The ApiConfig object is just a plain object — build it once and pass the same reference to every client. The auth library's token cache stays shared, so a refresh triggered by PetsClient is reused by StoreClient:

// src/api.ts
import { type ApiConfig } from './generated/base.js';
import { PetsClient } from './generated/pets-client.js';
import { StoreClient } from './generated/store-client.js';

export const apiConfig: ApiConfig = {
  baseUrl: 'https://api.example.com',
  headers: async () => {
    const result = await msalInstance.acquireTokenSilent({ scopes });
    return { Authorization: `Bearer ${result.accessToken}` };
  },
};

export const pets = new PetsClient(apiConfig);
export const store = new StoreClient(apiConfig);

Mutating the shared config later (e.g. swapping baseUrl for a different environment) is reflected by every client on its next request, since each client reads from this.config per call.

Method signatures

Generated methods follow a fixed argument order so the call site stays predictable:

client.<operationId>(
  ...pathParams,            // required, positional, in path order
  ...requiredHeaderParams,  // required headers/cookies, positional
  body,                     // if the operation has a request body
  params,                   // optional object — query parameters
  headerParams,             // optional object — optional headers
  cookieParams,             // optional object — optional cookies
  requestOptions,           // optional — { headers?, signal? }
);

Example for PUT /pets/{id} with a body, an optional ?expand= query, and a required X-Tenant-Id header:

const pet = await client.updatePet(
  123,                                    // path param
  'tenant-a',                             // required X-Tenant-Id header
  { name: 'Rex', tag: 'dog' },            // body
  { expand: 'owner' },                    // optional query params
  undefined,                              // no optional headers
  undefined,                              // no optional cookies
  { signal: ctrl.signal },                // requestOptions
);

Config options

| Option | Type | Default | Description | |--------|------|---------|-------------| | source | string | required | Path or URL to OpenAPI spec (JSON or YAML) | | target | string | required | Output directory | | codegen.dateType | 'string' \| 'Date' \| 'dayjs' | 'string' | How to type format: date/date-time | | codegen.enumType | 'constObject' \| 'union' \| 'enum' | 'constObject' | How to generate enums | | codegen.propertyNameStyle | 'original' \| 'camelCase' | 'original' | Property naming | | codegen.nullableType | 'null' \| 'undefined' | 'null' | How to represent nullable fields | | codegen.int64Type | 'number' \| 'bigint' \| 'string' | 'number' | How to type format: int64 | | codegen.clientSuffix | 'Client' \| 'Api' | 'Client' | Suffix on generated class names |

Features

  • OpenAPI 3.0 and 3.1 support (including $dynamicRef generics, prefixItems, const, type-array nullability)
  • allOf inheritance, oneOf/anyOf unions, discriminator tagged unions
  • Multipart / FormData uploads, binary (Blob) responses
  • Async dynamic headers (e.g. token refresh), per-request header overrides, AbortSignal cancellation
  • Typed error responses (ApiErrorBody) and ApiError class
  • Optional dayjs date handling with auto-generated reviver/replacer
  • JSDoc comments from OpenAPI descriptions, including @deprecated

Working with .NET specs

Method names from operationId. Methods are named after the spec's operationId. When it's absent (common in NSwag / Microsoft.AspNetCore.OpenApi output), the name falls back to the last non-template path segment, camelCased — /api/Dalux/GetMeters becomes getMeters.

Integer enum names (x-enum-varnames). OpenCodegen uses the x-enum-varnames extension to name integer enum members. Without it, members fall back to synthetic _0, _1, ... names.

{
  "Status": {
    "type": "integer",
    "enum": [0, 1, 2],
    "x-enum-varnames": ["Pending", "Active", "Archived"]
  }
}

export const Status = { Pending: 0, Active: 1, Archived: 2 } as const;
export type Status = typeof Status[keyof typeof Status];

Microsoft.AspNetCore.OpenApi doesn't emit x-enum-varnames out of the box — add a schema transformer that writes the C# enum member names into the extension before the document is serialized. NSwag's x-enumNames is not supported; pick a transformer that emits the OpenAPI Generator convention so the two ends agree.

Limitations

These cases are detected and rejected with a descriptive error rather than silently producing wrong output:

  • External file $ref — only internal #/components/... references are resolved. Inline external schemas or pre-merge them.
  • URL $ref — same; inline the referenced schema.
  • Swagger 2.0 — detected and rejected with a hint to convert via swagger2openapi. OpenAPI 3.0+ only.
  • Query parameter style: deepObject — not supported. Other styles (form, spaceDelimited, pipeDelimited) work.
  • Multipart request bodies with nested objectsFormData can't represent nested objects; flatten the schema or send application/json instead. Top-level scalars, Blob, and arrays of those are fine.

These are deliberate scope cuts at this stage — file an issue if one is blocking real work.

readOnly / writeOnly are currently treated as informational only. The same generated type is used for both request and response bodies, so server-generated fields like id or createdAt (commonly marked readOnly) appear on request types as well. Pass them as undefined on create requests, or use Omit<T, 'id'> at the call site.

License

MIT