astahmer/typed-openapi(新しいタブで開きます)
主要言語: TypeScript
Generate a headless Typescript API client from an OpenAPI spec - optionally with a @tanstack/react-query client using queryOptions
- star 数
- 402
- watcher 数
- 3
- fork 数
- 46
- issue 数
- 1
- 最終更新
- 2026/09/16
主要言語: TypeScript
Generate a headless Typescript API client from an OpenAPI spec - optionally with a @tanstack/react-query client using queryOptions
Generate a Typescript API client from an OpenAPI spec
See the documentation or try the online playground.
--default-fetcher — honors requestFormat for json /
form-data / form-url / binary / text bodies)queryOptions, suspenseQueryOptions, infiniteQueryOptions (with pageParamKey),
queryKeyFactory, and invalidate / invalidateEndpoint helpers — works with promise and Effect clients
(Effect.runPromise when needed)--msw): request handlers + a typed method/path mock facade from schemas/examples; optional --msw-faker for
@faker-js/fakersecuritySchemes: default fetcher emits AuthCredentials + configureFetcher({ getAuth }) (apiKey /
http / oauth2 / openIdConnect)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).--transform-dates, --transform-bigint): format: date-time|date → Date,
format: int64 → bigint (types + runtime schema transforms; none-runtime revives ISO date strings)defineConfig + typed-openapi.config.ts (also JSON); auto-loaded when present--client effect): methods return Effect with error channel
TypedStatusError | HttpClientError (non-status failures remapped; original in cause)--validate-side) with optional onValidate hook--coerce, default on when runtime ≠ none)$ref components stay shared — see stripReadWrite)text/event-stream → ReadableStream; co-declared JSON is unioned into response types) and binary
bodies/schemas (Blob)FetcherResponse (no DOM Response dependency; avoids clash with OAS ApiResponse schemas) +
request input types (InferSchemaInput / z.input / encoded).types.d.ts file, keeping TypeScript responsive for large specs; opt out with --no-runtime-types--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.
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.
// 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).
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 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.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 endpoint
Generate TanStack Query wrappers for your endpoints with:
npx typed-openapi api.yaml --tanstack
You get:
queryOptions / suspenseQueryOptions for useQuery / useSuspenseQueryinfiniteQueryOptions({ initialPageParam, getNextPageParam, pageParamKey? }) for paginationqueryKeyFactory + invalidate / invalidateEndpoint / invalidateInfinite for cache controlwithResponse and selectFn for advanced error and response handlingnpx typed-openapi api.yaml --msw
# optional: --msw-faker --msw-base-url https://api.example.comEmits 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 }), });
// 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", }); }, });
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 testPackage 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.