Generate a Typescript API client from an OpenAPI spec
See the documentation or try the online playground.
- Headless API client,
bring your own fetcher (fetch,
axios, ky, etc...) ! (You can generate that file with
--default-fetcher— honorsrequestFormatfor 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(withpageParamKey),queryKeyFactory, andinvalidate/invalidateEndpointhelpers — works with promise and Effect clients (Effect.runPromisewhen needed) - MSW mocks (
--msw): request handlers + mock factories from schemas/examples; optional--msw-fakerfor@faker-js/faker - Auth from
securitySchemes: default fetcher emitsAuthCredentials+configureFetcher({ getAuth })(apiKey / http / oauth2 / openIdConnect) - Date / bigint transforms (
--transform-dates,--transform-bigint):format: date-time|date→Date,format: int64→bigint(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):
- Effect-native client (
--client effect): methods returnEffectwith error channelTypedStatusError | HttpClientError(non-status failures remapped; original incause) - Validate input and/or output (
--validate-side) with optionalonValidatehook - 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
$refcomponents stay shared — seestripReadWrite) - SSE (
text/event-stream→ReadableStream; co-declared JSON is unioned into response types) and binary bodies/schemas (Blob) - Node-friendly
FetcherResponse(no DOMResponsedependency; avoids clash with OASApiResponseschemas) + 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.tsfile, keeping TypeScript responsive for large specs; opt out with--no-runtime-types - Filter endpoints/schemas (
--endpoint/--schema, orendpointPatterns/schemaPatternsin 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.
pnpm add typed-openapiIt 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.
// 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).
npx typed-openapi -htyped-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
-
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.
The generated client is headless - you need to provide your own fetcher. Here are ready-to-use examples:
- Basic API Client - Simple, dependency-free wrapper
- Validating API Client - With request/response validation
Or generate one with --default-fetcher (recommended).
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).
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.
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 endpointGenerate TanStack Query wrappers for your endpoints with:
npx typed-openapi api.yaml --tanstackYou get:
- Type-safe queries and mutations with full error inference
queryOptions/suspenseQueryOptionsforuseQuery/useSuspenseQueryinfiniteQueryOptions({ initialPageParam, getNextPageParam, pageParamKey? })for paginationqueryKeyFactory+invalidate/invalidateEndpoint/invalidateInfinitefor cache controlwithResponseandselectFnfor advanced error and response handling- All mutation errors are Response-like and type-safe, matching your OpenAPI error schemas
npx typed-openapi api.yaml --msw
# optional: --msw-faker --msw-base-url https://api.example.comEmits msw.handlers.ts with handlers and per-endpoint get…Mock() factories (schema examples/defaults, or faker).
With --default-fetcher, credentials from OAS components.securitySchemes are wired as:
import { configureFetcher } from "./api.client";
configureFetcher({
getAuth: () => ({ petstore_auth: token, api_key: key }),
});// 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,
);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,
);// 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);
}
},
},
);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",
});
},
});- 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.
pnpm ipnpm buildpnpm 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.