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 — 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 + a typed method/path mock facade from schemas/examples; optional --msw-faker for
@faker-js/faker
- Auth from
securitySchemes: default fetcher emits AuthCredentials + configureFetcher({ getAuth }) (apiKey /
http / oauth2 / openIdConnect)
- Closed objects by default: omitted
additionalProperties is treated as false
so generated types and validators don't silently accept unknown keys. Opt into the
OpenAPI/JSON Schema default with --openapi-additional-properties-default /
openapi: { additionalPropertiesDefault: true }. Closed allOf members compose
(combined keys are accepted; unknown keys are rejected).
- 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):
- 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-stream → ReadableStream; 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.
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,
});
Objects are closed when additionalProperties is omitted. Set openapi.additionalPropertiesDefault: true to follow the OpenAPI default (true when omitted):
export default defineConfig({
input: "./openapi.yaml",
openapi: { additionalPropertiesDefault: 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).
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 and a typed method/path facade, 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
--openapi-additional-properties-default When additionalProperties is omitted, use OpenAPI/JSON Schema's default (`true`). Default: omitted additionalProperties is `false` so named objects stay closed
-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:
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:
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 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
npx typed-openapi api.yaml --msw
# optional: --msw-faker --msw-base-url https://api.example.com
Emits msw.handlers.ts with the standard handlers array plus a typed mock facade. Resolve a response or create an
overrideable handler by OpenAPI method/path, without one top-level export per endpoint:
import { HttpResponse } from "msw";
import { handlers, mock } from "./msw.handlers";
const pet = mock.get("/pet/{petId}").response();
const customPetHandler = mock.get("/pet/{petId}").handler(() => HttpResponse.json(pet));
server.use(...handlers, customPetHandler);
Responses use schema examples/defaults (or faker with --msw-faker), and the generated MswPath / MswResponse types
keep method/path and response inference aligned with the OpenAPI document.
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,
);
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);
}
},
},
);
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",
});
},
});
- 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 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.