Skip to content

astahmer/typed-openapi

Repository files navigation

typed-openapi

Generate a Typescript API client from an OpenAPI spec

See the online playground

Screenshot 2023-08-08 at 00 48 42

pkg.pr.new

Features

  • Headless API client, bring your own fetcher (fetch, axios, ky, etc...) ! (You can generate that file with --default-fetcher — honors requestFormat for json / form-data / form-url / binary / text bodies)
  • Generates a fully typesafe API client with just types by default (instant suggestions)
  • Type-safe error handling: with discriminated unions and configurable success/error status codes
  • withResponse & throwOnStatusError: Get a union-style response object or throw on configured error status codes, with full type inference
  • TanStack Query integration: queryOptions, suspenseQueryOptions, infiniteQueryOptions (with pageParamKey), queryKeyFactory, and invalidate / invalidateEndpoint helpers — works with promise and Effect clients (Effect.runPromise when needed)
  • MSW mocks (--msw): request handlers + mock factories from schemas/examples; optional --msw-faker for @faker-js/faker
  • Auth from securitySchemes: default fetcher emits AuthCredentials + configureFetcher({ getAuth }) (apiKey / http / oauth2 / openIdConnect)
  • Date / bigint transforms (--transform-dates, --transform-bigint): format: date-time|dateDate, format: int64bigint (types + runtime schema transforms; none-runtime revives ISO date strings)
  • Typed config: defineConfig + typed-openapi.config.ts (also JSON); auto-loaded when present
  • Or you can also generate a client with runtime validation using one of the following runtimes (first-party adapters):
    • zod v4 (--runtime zod) and v3 (--runtime zod3)
    • effect Schema v4 (--runtime effect) and v3 / @effect/schema (--runtime effect3)
    • valibot (--runtime valibot)
    • arktype (--runtime arktype)
    • TypeBox (--runtime typebox) and typia (--runtime typia)
  • Effect-native client (--client effect): methods return Effect with error channel TypedStatusError | HttpClientError (non-status failures remapped; original in cause)
  • Validate input and/or output (--validate-side) with optional onValidate hook
  • Coerce path/query/cookie/header primitives from strings (--coerce, default on when runtime ≠ none)
  • Cookie parameters, OAS defaults on runtime schemas, readOnly/writeOnly stripping (inlined objects; named $ref components stay shared — see stripReadWrite)
  • SSE (text/event-streamReadableStream; co-declared JSON is unioned into response types) and binary bodies/schemas (Blob)
  • Node-friendly FetcherResponse (no DOM Response dependency; avoids clash with OAS ApiResponse schemas) + request input types (InferSchemaInput / z.input / encoded)
  • Runtime type sidecar (default for runtime clients): emitted validators live in one module while their public types live in a sibling .types.d.ts file, keeping TypeScript responsive for large specs; opt out with --no-runtime-types
  • Filter endpoints/schemas (--endpoint / --schema, or endpointPatterns / schemaPatterns in config) and control naming (--schema-naming)

The main generated client is one file that can be used in the browser or in node. When a runtime is selected, a sibling .types.d.ts sidecar is emitted by default so TypeScript stays responsive for large specs. Keep the sidecar with the runtime module when committing, publishing, or copying generated output. Use --no-runtime-types when you need a single checked file instead. Runtime schemas are emitted by typed-openapi's own Schema IR + runtime adapters (no Sinclair codegen). Use --validation loose|formats|strict (or a fine-grained policy object) to control how deep OpenAPI constraints (format, minLength, …) are applied. Install only the runtime selected for your generated client as a dependency in your app.

With --runtime effect / effect3, the default API client is Effect-native (--client effect); other runtimes default to the promise ApiClient. Install effect whenever you use --client effect and/or --runtime effect. Use --runtime effect3 (+ @effect/schema) if you need the Effect Schema v3 adapter.

HTTP(S) OpenAPI inputs are supported; use --input-dir <dir> to choose where the default output file is written.

Install & usage

pnpm add typed-openapi

It exports a bunch of functions that can be used to build your own tooling on top of it. You can look at the CLI code so see how to use them.

Config file (defineConfig)

// typed-openapi.config.ts
import { defineConfig } from "typed-openapi";

export default defineConfig({
  // input: "./openapi.yaml", // optional — CLI positional still preferred when passed
  output: "./src/api/client.ts",
  runtime: "zod",
  validation: "strict",
  validateSide: "both",
  defaultFetcher: true,
  tanstack: "query.ts",
  msw: "msw.ts",
  transformDates: true,
  format: true,
});

defaultFetcher can also be an object to customize the generated fetcher:

export default defineConfig({
  input: "./openapi.yaml",
  output: "./src/api/openapi.ts",
  defaultFetcher: {
    clientPath: "./openapi.ts",
    envApiBaseUrl: "VITE_API_URL",
    fetcherName: "fetchApi",
    apiName: "api",
  },
});

Use a fine-grained validation policy when a preset needs an exception:

export default defineConfig({
  input: "./openapi.yaml",
  runtime: "zod",
  validation: { preset: "strict", stringConstraints: false, numberConstraints: false },
});

JSON (typed-openapi.config.json) still works. The CLI auto-loads typed-openapi.config.ts, .mts, .js, .mjs, .json, .typed-openapi.json, or typed-openapi.json from the cwd. typed-openapi.config.ts is loaded via tsx (shipped as a dependency).

CLI

npx typed-openapi -h
typed-openapi/3.0.0

Usage:
  $ typed-openapi [input]

Commands:
  [input]  Generate (OpenAPI path; optional when `input` is set in config)

For more info, run any command with the `--help` flag:
  $ typed-openapi --help

Options:
  -o, --output <path>             Output path for the api client ts file (defaults to `<input>.<runtime>.ts`)
  --input-dir <path>              Directory for the default output when the OpenAPI input is an HTTP(S) URL
  -r, --runtime <n>               Runtime to use for validation; defaults to `none` (or config file); available: Type<"arktype" | "effect" | "effect3" | "none" | "typebox" | "typia" | "valibot" | "zod" | "zod3">
  --validation <level>            Validation depth for runtime schemas: `loose` (structure only), `formats` (+ email/uuid/…), `strict` (+ min/max/pattern/…). Default: `strict` when runtime ≠ none
  -c, --config <path>             Path to typed-openapi config (JSON or TS/JS with defineConfig); defaults to typed-openapi.config.ts / .json when present
  --format                        Format generated files with oxfmt (defaults to false)
  --schemas-only                  Only generate schemas, skipping client generation (defaults to false)
  --include-client                Include API client types and implementation (defaults to true)
  --jsdoc                         Emit OpenAPI descriptions as JSDoc comments (defaults to true)
  --success-status-codes <codes>  Comma-separated list of success status codes (defaults to 2xx and 3xx ranges)
  --error-status-codes <codes>    Comma-separated list of error status codes (defaults to 4xx and 5xx ranges)
  --tanstack [name]               Generate tanstack client, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file
  --msw [name]                    Generate MSW request handlers, defaults to false; optional output name/path next to the main file
  --msw-faker                     Use @faker-js/faker in generated MSW mock factories (requires the package installed)
  --msw-base-url <url>            Base URL/prefix for MSW handlers (default: "*")
  --default-fetcher [name]        Generate default fetcher, defaults to false, can optionally specify a name (will be generated next to the main file) or absolute path for the generated file
  --endpoint <regex>              Keep endpoints matching regex (method/path/operationId/alias/tags); repeatable
  --schema <regex>                When tree-shaking, also keep schemas matching regex (name/ref); repeatable
  --tree-shake-schemas            Drop unused component schemas (default: on when --endpoint is set)
  --no-tree-shake-schemas         Emit all component schemas even when filtering endpoints (default: true)
  --schema-naming <mode>          Component schema naming: auto | always-name | prefer-inline (inline single-use non-recursive schemas)
  --client <kind>                 API client style: promise | effect (default: effect when runtime is effect/effect3, else promise)
  --validate-side <side>          When using a runtime: none | input | output | both (default: both)
  --coerce                        Coerce number/boolean path|query|cookie|header params from strings (default: on when runtime ≠ none)
  --no-coerce                     Disable string coercion for path|query|cookie|header params
  --transform-dates               Map format date-time/date to Date (types + runtime transforms; none-runtime revives ISO strings)
  --transform-bigint              Map format int64 to bigint (types + runtime transforms)
  --runtime-types                 Generate a .types.d.ts sidecar for runtime client types (default for runtime clients)
  --no-runtime-types              Do not generate a runtime type declaration sidecar
  -h, --help                      Display this message
  -v, --version                   Display version number

Non-goals

  • Shipping every historical runtime (yup / io-ts). TypeBox and Typia are available again via --runtime typebox / --runtime typia; the adapter contract makes further runtimes easy to add. Primary focus remains zod, effect, valibot, and arktype.

  • Being a full JSON Schema validator suite for exotic media types. Constraints (format, bounds, patterns, …) are supported via --validation, but the priority remains a fast, typesafe API client.

  • Splitting the generated client into multiple files. Nope. Been there, done that. Let's keep it simple.

Basically, let's focus on having a fast and typesafe API client generation instead.

Usage Examples

API Client Setup

The generated client is headless - you need to provide your own fetcher. Here are ready-to-use examples:

Or generate one with --default-fetcher (recommended).

Fetcher requestFormat contract

Fetcher.fetch receives requestFormat so the body can be encoded from the OpenAPI requestBody content type. Missing entries in endpointRequestFormats default to "json".

requestFormat Body encoding (default fetcher)
json JSON.stringify + application/json
form-data FormData (let fetch set the multipart boundary)
form-url URLSearchParams + application/x-www-form-urlencoded
binary raw Blob / ArrayBuffer / Uint8Array / string + application/octet-stream
text String(body) + text/plain

Custom fetchers should honor the same table (or document if they only support JSON).

Type-Safe Error Handling & Response Modes

You can choose between two response styles:

  • Direct data return (default):

    const user = await api.get("/users/{id}", { path: { id: "123" } });
    // Throws TypedStatusError on error status (default)
  • Union-style response (withResponse):

    const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
    if (result.ok) {
      // result.data is typed as User
    } else {
      // result.data is typed as your error schema for that status
    }

You can also control error throwing with throwOnStatusError.

All errors thrown by the client are instances of TypedStatusError and include the parsed error data.

Generic Request Method

For dynamic endpoint calls or when you need more control:

// Type-safe generic request method
const response = await api.request("GET", "/users/{id}", {
  path: { id: "123" },
  query: { include: ["profile", "settings"] },
});

const user = await response.json(); // Fully typed based on endpoint

TanStack Query Integration

Generate TanStack Query wrappers for your endpoints with:

npx typed-openapi api.yaml --tanstack

You get:

  • Type-safe queries and mutations with full error inference
  • queryOptions / suspenseQueryOptions for useQuery / useSuspenseQuery
  • infiniteQueryOptions({ initialPageParam, getNextPageParam, pageParamKey? }) for pagination
  • queryKeyFactory + invalidate / invalidateEndpoint / invalidateInfinite for cache control
  • withResponse and selectFn for advanced error and response handling
  • All mutation errors are Response-like and type-safe, matching your OpenAPI error schemas

MSW mocks

npx typed-openapi api.yaml --msw
# optional: --msw-faker --msw-base-url https://api.example.com

Emits msw.handlers.ts with handlers and per-endpoint get…Mock() factories (schema examples/defaults, or faker).

Auth (securitySchemes)

With --default-fetcher, credentials from OAS components.securitySchemes are wired as:

import { configureFetcher } from "./api.client";

configureFetcher({
  getAuth: () => ({ petstore_auth: token, api_key: key }),
});

useQuery / fetchQuery / ensureQueryData

// Basic query
const accessiblePagesQuery = useQuery(tanstackApi.get("/authorization/accessible-pages").queryOptions);

// Query with query parameters
const membersQuery = useQuery(
  tanstackApi.get("/authorization/organizations/:organizationId/members/search", {
    path: { organizationId: "org123" },
    query: { searchQuery: "john" },
  }).queryOptions,
);

// With additional query options
const departmentCostsQuery = useQuery({
  ...tanstackApi.get("/organizations/:organizationId/department-costs", {
    path: { organizationId: params.orgId },
    query: { period: selectedPeriod },
  }).queryOptions,
  staleTime: 30 * 1000,
  // placeholderData: keepPreviousData,
  // etc
});

or if you need it in a router beforeLoad / loader:

import { tanstackApi } from "#api";

await queryClient.fetchQuery(
  tanstackApi.get("/:organizationId/remediation/accounting-lines/metrics", {
    path: { organizationId: params.orgId },
  }).queryOptions,
);

useMutation

The mutation API supports both basic usage and advanced error handling with withResponse and custom transformations with selectFn. Note: All mutation errors are Response-like objects with type-safe error inference based on your OpenAPI error schemas.

// Basic mutation (returns data only)
const basicMutation = useMutation({
  // Will throw TypedStatusError on error status
  ...tanstackApi.mutation("post", "/authorization/organizations/:organizationId/invitations").mutationOptions,
  onError: (error) => {
    // error is a Response-like object with typed data based on OpenAPI spec
    console.log(error instanceof Response); // true
    console.log(error.status); // 400, 401, etc. (properly typed)
    console.log(error.data); // Typed error response body
  },
});

// With error handling using withResponse
const mutationWithErrorHandling = useMutation(
  tanstackApi.mutation("post", "/users", {
    // Returns union-style result, never throws
    withResponse: true,
  }).mutationOptions,
);

// With custom response transformation
const customMutation = useMutation(
  tanstackApi.mutation("post", "/users", {
    selectFn: (user) => ({ userId: user.id, userName: user.name }),
  }).mutationOptions,
);

// Advanced: withResponse + selectFn for comprehensive error handling
const advancedMutation = useMutation(
  tanstackApi.mutation("post", "/users", {
    withResponse: true,
    selectFn: (response) => ({
      success: response.ok,
      user: response.ok ? response.data : null,
      error: response.ok ? null : response.data,
      statusCode: response.status,
    }),
  }).mutationOptions,
);

Usage Examples:

// Basic usage
basicMutation.mutate({
  body: {
    emailAddress: "user@example.com",
    department: "engineering",
    roleName: "admin",
  },
});

// With error handling
// All errors thrown by mutations are type-safe and Response-like, with parsed error data attached.
mutationWithErrorHandling.mutate(
  { body: userData },
  {
    onSuccess: (response) => {
      if (response.ok) {
        toast.success(`User ${response.data.name} created!`);
      } else {
        if (response.status === 400) {
          toast.error(`Validation error: ${response.data.message}`);
        } else if (response.status === 409) {
          toast.error("User already exists");
        }
      }
    },
  },
);

// Advanced usage with custom transformation
advancedMutation.mutate(
  { body: userData },
  {
    onSuccess: (result) => {
      if (result.success) {
        console.log("Created user:", result.user.name);
      } else {
        console.error(`Error ${result.statusCode}:`, result.error);
      }
    },
  },
);

useMutation without the tanstack api

If you need to make a custom mutation you could use the api directly:

const { mutate: login, isPending } = useMutation({
  mutationFn: async (type: "google" | "microsoft") => {
    return api.post(`/authentication/${type}`, { body: { redirectUri: search.redirect } });
  },
  onSuccess: (data) => {
    window.location.replace(data.url);
  },
  onError: (error, type) => {
    console.error(error);
    toast({
      title: t(`toast.login.${type}.error`),
      icon: "warning",
      variant: "critical",
    });
  },
});

Alternatives

  • openapi-typescript + openapi-fetch — types-only + thin fetch
  • Orval — React Query / MSW / mocks, multi-file output
  • Hey API — plugin SDK generator (clients, validators, hooks)
  • Kubb — modular plugin codegen (TanStack, SWR, MSW, Zod)
  • openapi-zod-client — zodios client (can be slow for large specs)

typed-openapi focuses on a single-file, headless client with multi-runtime validation (zod / effect / valibot / arktype / …), typed status errors, and optional TanStack / MSW extras — without splitting output by tags.

Contributing

  • pnpm i
  • pnpm build
  • pnpm test

Package tests live under packages/typed-openapi/tests/:

Folder What
integrations/ MSW e2e + runtime client matrix
github-issues/ Regression coverage for fixed GitHub issues
tstyche/ Per-runtime + Effect client type suites
samples/ OpenAPI fixtures (including samples/github-issues/)

When you're done with your changes, please run pnpm changeset in the root of the repo and follow the instructions described here.

About

Generate a headless Typescript API client from an OpenAPI spec - optionally with a @tanstack/react-query client using queryOptions

Topics

Resources

License

Stars

343 stars

Watchers

3 watching

Forks

Contributors