Generate a Typescript API client from an OpenAPI spec
- Headless API client, bring your own fetcher (fetch, axios, ky, etc...) ! (You can generate that file with
--default-fetcher
) - 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: with
withResponse
andselectFn
options for advanced success/error handling - Or you can also generate a client with runtime validation using one of the following runtimes:
The generated client is a single file that can be used in the browser or in node. Runtime validation schemas are provided by the excellent typebox-codegen
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.
npx typed-openapi -h
typed-openapi/2.0.0
Usage:
$ typed-openapi <input>
Commands:
<input> Generate
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`)
-r, --runtime <n> Runtime to use for validation; defaults to `none`; available: Type<"arktype" | "io-ts" | "none" | "typebox" | "valibot" | "yup" | "zod"> (default: none)
--schemas-only Only generate schemas, skipping client generation (defaults to false) (default: false)
--include-client Include API client types and implementation (defaults to true) (default: 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
--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
-h, --help Display this message
-v, --version Display version number
-
Caring too much about the runtime validation code. If that works (thanks to typebox-codegen), that's great, otherwise I'm not really interested in fixing it. If you are, feel free to open a PR.
-
Supporting all the OpenAPI spec. Regex, dates, files, whatever, that's not the point here. openapi-zod-client does a great job at that, but it's slow to generate the client and the suggestions in the IDE are not instant. I'm only interested in supporting the subset of the spec that makes the API client typesafe and fast to provide suggetions in the IDE.
-
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
You can choose between two response styles:
-
Direct data return (default):
const user = await api.get("/users/{id}", { path: { id: "123" } }); // Throws TypedResponseError 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 TypedResponseError
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
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
withResponse
andselectFn
for advanced error and response handling- All mutation errors are Response-like and type-safe, matching your OpenAPI error schemas
// 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 throws TypedResponseError 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-zod-client, which generates a zodios client but can be slow to provide IDE suggestions when the OpenAPI spec is large. Also, you might not always want to use zod or even runtime validation, hence this project.
pnpm i
pnpm build
pnpm test
When you're done with your changes, please run pnpm changeset
in the root of the repo and follow the instructions
described here.