← Default view
Evaluation datasheet Rev. 2026-07-14 Survey depth 30 sources 10 min

Four generators.
One spec. One winner.

Bench test of every OpenAPI → TypeScript client generator worth naming in 2026, scored against one target: a typed TanStack Query client over a .NET 10 backend that answers errors in ProblemDetails.

Subjectitenium-ui
Backend.NET 10 · OAS 3.1
Error contractProblemDetails
MonorepoNx 21 · bun
One OpenAPI schema document refracted through a prism into four differently shaped generated clients
Fig. 1 — One document, four refractions. Every generator below inherits whatever Microsoft.AspNetCore.OpenApi hands it.
◆ RULING ◆

ADOPT openapi-typescript
+ openapi-fetch + openapi-react-query

It is the only option where useQuery().error is already your ProblemDetails union with no wiring, where query keys are DataTag-branded so setQueryData is typed for optimistic updates [21], and where nothing but a .d.ts file lands in the diff [18].

DECISION
REJECT
KIOTA
TYPESCRIPT = PREVIEW
NO TANSTACK LAYER
01

The field

Ranked by fitness for this target — not by popularity. Bars show npm downloads for the week of 5–11 Jul 2026, scaled against the leader.
01 Adopt
⭐ 8.2k + openapi-fetch + openapi-react-query

Types-only codegen; the runtime is ~7 kB of wrapper. The error channel is a discriminated union, so TypeScript narrows ProblemDetails for free [19]. Cheapest exit of the four.

5.97M
npm / week · openapi-fetch [28]
02 Runner-up
⭐ 5.1k MIT 20+ plugins

Emits real queryOptions() objects and types the error channel straight from the spec [14]. Take it if you want named SDK functions, Zod validators, or a non-React consumer.

2.88M
npm / week [29]
03 Viable, backward
⭐ 6.2k MSW + Faker ⚠ CVE-2026-24132

Still hand-rolls option objects instead of calling TanStack's helper, so its suspense options don't typecheck [7]. Adopting it means adopting the pattern TanStack is drifting away from.

1.29M
npm / week [29]
04 Reject
⭐ 3.8k runtime repo ⭐ 61 no npm

Microsoft's own README marks TypeScript preview [22] — their ladder defines that as "isn't being used to generate production API clients" [23]. No TanStack story at all.

47.9k
npm / week · kiota-bundle [29]
02

Capability matrix

Eleven axes, scored. ★ marks the four the decision actually turned on — the rest are tiebreakers, not tests.
Axis
11 tests · 4 decisive
01 · ADOPT
⭐ 8.2k · stack of 3
02 · RUNNER-UP
⭐ 5.1k · MIT
03 · VIABLE
⭐ 6.2k
04 · REJECT
⭐ 3.8k · preview
Client shapeWhat you actually call
queryOptions + hooksruntime wrapper, no codegen [20]
queryOptions · mutationOptions · queryKeygenerated [12]
one hook per endpointand only a hook [6]
fluent request buildersclient.posts.byPostId(id).get() [24]
Calls TanStack's queryOptions() helperNot a lookalike literal
Yes
Literally imports it [13]
Hand-typed literals; issue open & unassigned [7]
No TanStack layer exists
ProblemDetails typed on errorThe deciding axis
Automatic — the exact union of declared 4xx/5xx bodies [19]
Automatic — resolves the operation's error symbol [14]
YOUR WIRING
You hand-write ErrorType<E> in the mutator [8]
HAND-CARRIED
Thrown ApiError subclasses; nothing carries the type into a hook
Typed setQueryDataOptimistic updates
Keys branded DataTag<key, data, error> — no generics needed [21]
setQueryData helper generated; bespoke object key
Key function only — the typing is yours to write
None
useSuspenseQueryRouter loaders, streaming
Supported [20]
Supported
Generated options do not typecheck against it [7]
None
Generated code in the diffYour review surface
types onlyone .d.ts — a reviewer can read it [18]
SDK + types + query layermechanical, large
hooks + types (+ mocks)thousands of regenerated lines
full client treeplus adapters and providers
Runtime weightShipped to the browser
6 kB + ~1 kBfetch wrapper + query binding [19]
small fetch client
axios or fetch
abstractions + adapterplus an auth provider [24]
Mocks / validatorsThe one axis Orval wins
None
Zod + 20 plus plugins [16]
MSW + Faker — nobody else matches it [9]
None
Installable via bun / npmNx 21 CI job
One devDependency
One devDependency
One devDependency
Binary, Docker, or dotnet tool — a second toolchain in CI [25]
npm downloads / week5–11 Jul 2026
5.97Mopenapi-fetch [28]
2.88M@hey-api/openapi-ts [29]
1.29Morval [29]
47.9kkiota-bundle [29]
MaturityVendor's own label
stableone monorepo, shared maintainers [30]
stable · MITVercel, PayPal, AWS [16]
stableRCE — CVE-2026-24132 fixed in 7.20.0 / 8.0.3 [10]
TYPESCRIPT = PREVIEWNOT FOR PRODUCTION CLIENTS — Microsoft's own ladder [23]
Ruling
ADOPTZero wiring on the axis that matters
RUNNER-UPSwitch here if you need Zod or mocks
VIABLE, BACKWARDOnly if MSW mocks outrank all else
REJECTPreview + no TanStack + no npm
deciding axis passes on the generator's own output outlined = passes, but you write the wiring partial / does not typecheck absent
03

Pre-flight — your document is the real problem

Fix these two on the .NET side before you pick a tool, or you will blame the tool for the backend's output.

Defect 1 · Numbers arrive as number | string

ASP.NET Core's default JsonNumberHandling.AllowReadingFromString makes the schema honest about what it will accept — so an int is emitted as a type array with a regex pattern [2]. Every TypeScript generator faithfully reproduces the union, and your whole model tree rots.

"type": ["integer", "string"],
"pattern": "^-?(?:0|[1-9]\d*)$"

// fix it at the source
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.NumberHandling = JsonNumberHandling.Strict);

Defect 2 · ProblemDetails extensions are invisible

The generated schema carries only the five RFC 7807 fields and no additionalProperties. The ASP.NET team closed the report as answered rather than fixing it [3]. So problem.Extensions["traceId"] will never appear in any generated type — no matter which tool you choose.

Workaround: add an IOpenApiSchemaTransformer that widens the schema, or declare a custom error type and ProducesProblem<MyProblem>.

And regardless of tool: declare your 4xx/5xx responses on every endpoint [1]. An undeclared error response means every generator here falls back to Error or unknown.

04

The shape question — hooks vs queryOptions

Not a stylistic quibble. It decides what you can do.
There's nothing wrong with calling useQuery in your component directly. Separating QueryKey from QueryFunction was a mistake. TkDodo · TanStack Query maintainer [4]

Orval — a hook, and only a hook

const { data } = useGetPetById(petId);

// prefetch in a router loader?
// suspend? rebuild the
// options by hand.

Hey API — a real queryOptions object

const { data } = useQuery(
  getPetByIdOptions({ path: { petId } }));

queryClient.prefetchQuery(
  getPetByIdOptions({ path: { petId } }));
// same object. works.

openapi-react-query — same shape, no codegen

const { data } = useQuery(
  $api.queryOptions(
    'get', '/pets/{petId}',
    { params: { path: { petId } } }));

Orval does emit getXQueryOptions() functions — but they are hand-typed UseQueryOptions literals rather than calls to TanStack's helper [5], which is why its generated suspense options don't typecheck. The issue is open and unassigned [7]. Adopting Orval today means adopting the pattern TanStack is drifting away from, and betting that Orval catches up.

05

The error channel · deciding axis

What useQuery().error is actually typed as, and who wired it.
openapi-fetch
openapi-react-query
the exact union of declared 4xx/5xx bodies
A { data, error } discriminated union [19]. The queryFn throws the parsed body and TError is inferred from it [21]. Because the error is a value, not a thrown unknown, TypeScript narrows it for free — HttpValidationProblemDetails and plain ProblemDetails land as a union you switch on by status.
Hey API
the operation's generated XError type
useTypeError() resolves the operation's error symbol, falling back to DefaultError only when the spec declares none [14].
Orval
ErrorType<T> — whatever your mutator says
You hand-write export type ErrorType<E> = AxiosError<E> in the custom mutator [8]. It works — but it is your wiring, a file you maintain, not the generator's output.
Kiota
thrown ApiError subclasses
Error mapping, caught in try/catch. There is no TanStack integration to carry the type into a hook.

⚠ Caveat · openapi-react-query

It throws the raw body, not an Error instance [21]. error.detail works; error.message does not. A network failure still surfaces as a TypeError the types don't predict — narrow before you trust it.

⚠ Caveat · Hey API

It used to stringify the error body into Error.message under throwOnError, throwing the typing away at runtime. Reported and fixed [15] — but worth a smoke test on your own spec.

06

Autopsy — why Kiota is out

The natural instinct for a .NET shop, and the wrong one here.

Preview means preview

Microsoft's maturity ladder defines Preview as "at or near the level of stable but isn't being used to generate production API clients" [23]. The README marks TypeScript at exactly that level [22]. The runtime libraries the generated client depends on live in a repo with ⭐ 61 [26].

The shape is wrong for React

Kiota gives you client.posts.byPostId(id).get() behind a request adapter and an auth provider [24]. There is no TanStack integration — you would hand-write a queryOptions wrapper and a query key for every endpoint. Which is precisely the work you are trying to generate away.

It doesn't install from npm

Your Nx CI job has to fetch a binary, run Docker, or install the .NET SDK [25]. A second toolchain in a Node pipeline, for a client 48k people a week download against openapi-fetch's ~6M.

07

Operations · Nx 21, CI, and the exit

All three viable options are npm packages driven by one config file — the Nx wiring is identical. What differs is the diff and the cost of walking away.

The only CI decision that matters

A cacheable generate-api target with the OpenAPI document as its only input and the generated directory as its output. Cache hits are free; the spec changing is the only thing that reruns it.

Commit the generated code. The diff is your review surface for backend changes, editors resolve types with no build step, and CI just verifies. Guard drift two ways:

# plain
bunx orval && git diff --exit-code

# or Nx's first-class equivalent —
# register codegen as a sync generator
nx sync:check

Nx fails the build when the client is stale [27]. This is where types-only quietly wins: a backend change produces a diff a reviewer can read, instead of a few thousand lines of regenerated hooks.

Optimistic updates · is the key exposed, is setQueryData typed?

openapi-react-query — the key is ['get', '/pets/{petId}', init], branded DataTag<key, data, error> [21]. setQueryData infers data and error with no generics. Invalidating by path prefix is a plain array prefix.

Hey API — generates xQueryKey() plus setQueryData/getQueryData helpers. The key is a bespoke object, so prefix invalidation runs through its tags mechanism rather than the array-prefix idiom you already know.

Orval — gives you getXQueryKey() and the raw fetcher. setQueryData typing is yours to write.

Lock-in ladder — ranked by cost of walking away

openapi-typescriptCheapest exit
The .d.ts is the asset; openapi-fetch and openapi-react-query are ~7 kB of wrapper on top. Drop the TanStack binding and your types survive untouched. All three ship from one monorepo under the openapi-ts org [30] — the binding isn't an orphan side-project. Note that openapi-react-query is the least-used piece at ~311k weekly downloads against openapi-fetch's ~6M.
Hey APIReal but mechanical
MIT, real corporate usage (Vercel, PayPal, AWS), and the hosted Platform is strictly optional — input takes a file path or URL just as happily as a registry reference [16][17]. Lock-in is the generated SDK call sites: real, but mechanical to replace.
OrvalHealthy, with an asterisk
⭐ 6.2k and actively pushed, and its MSW + Faker mock generation is a genuine feature nobody else matches [9]. ⚠ It also shipped an RCE: CVE-2026-24132 (CVSS 7.7) let a malicious OpenAPI document inject TypeScript into generated mock files; fixed in 7.20.0 / 8.0.3. With a first-party .NET spec the threat model barely applies — but pin the patched version.
08

The order

What to do on Monday.

Do this

For itenium-ui: openapi-typescript + openapi-fetch + openapi-react-query, with JsonNumberHandling.Strict and ProducesProblem<T> on every endpoint on the .NET side. You get the composable-queryOptions shape TanStack is converging on, a ProblemDetails union on error that needs zero wiring, typed optimistic updates via DataTag, and a one-file diff per backend change.

Switch to Hey API if…

…you want generated Zod validators at the boundary, you want MSW-style mocks without adopting Orval, or you want the same client consumed from something other than React. It costs you a larger generated tree and a bespoke query-key shape. It costs you nothing on error typing.

Pick Orval only if…

…generated MSW + Faker mocks are the requirement that outranks everything else. Accept that you are adopting the one-hook-per-endpoint model TanStack's own maintainer has moved on from, with suspense support that doesn't currently typecheck [7].

09

Source ledger

30 citations. Official docs, source files, issue threads and one CVE.
01ASP.NET Core OpenAPI supportofficial · .NET 10 defaults to OpenAPI 3.1; ProducesProblem attaches the metadata every generator hangs its error typing off.
02The .NET 10 OpenAPI number quirkvendor-blog · integers emitted as string|integer unions.
03dotnet/aspnetcore #62099forum · ⭐ 38k · ProblemDetails extensions absent from the schema; closed as answered.
04The Query Options API — TkDodovendor-blog · the maintainer's case against a wrapper hook per endpoint.
05queryOptions — TanStack Query v5official · the type-safe unit shared between hooks and QueryClient methods.
06Orval — React Query guideofficial · one custom hook per operation.
07orval #1788 — suspense options unusableforum · ⭐ 6.2k · open, unassigned.
08Orval — custom axios mutatorofficial · where you hand-write ErrorType.
09Orval — output configurationofficial · mock: true generates MSW handlers + Faker data.
10CVE-2026-24132 — NVDofficial · CVSS 7.7 · TypeScript injection into Orval's generated mocks.
11orval #1817 — OpenAPI 3.1 type arraysforum · ⭐ 6.2k · 3.1 nullability lands correctly.
12Hey API — TanStack Query pluginofficial · emits queryOptions / mutationOptions / queryKey, not hooks.
13hey-api — generated react-query.gen.tsofficial · ⭐ 5.1k · imports TanStack's real queryOptions helper.
14hey-api — query-core use-type.tsofficial · ⭐ 5.1k · TError resolves to the operation's error symbol.
15hey-api/openapi-ts #1029forum · ⭐ 5.1k · throwOnError once stringified the error body; fixed.
16hey-api/hey-apiofficial · ⭐ 5.1k · MIT, 20+ plugins, used by Vercel / PayPal / AWS.
17Hey API — get startedofficial · the hosted Platform is optional.
18openapi-typescript — introductionofficial · runtime-free types only; OAS 3.0 and 3.1.
19openapi-fetch — docsofficial · 6 kB; returns a { data, error } discriminated union.
20openapi-react-query — docsofficial · ~1 kB wrapper; not a generator.
21openapi-react-query — src/index.tsofficial · ⭐ 8.2k · queryFn throws the parsed body; keys are DataTag-branded.
22microsoft/kiotaofficial · ⭐ 3.8k · README marks TypeScript as preview.
23Kiota — language support & maturityofficial · "Preview … isn't being used to generate production API clients."
24Kiota — TypeScript quickstartofficial · fluent request builders, all-optional models, adapter + auth provider.
25Kiota — installofficial · binary, Docker, or dotnet tool. No npm.
26microsoft/kiota-typescriptofficial · ⭐ 61 · the runtime the generated client depends on.
27Nx — commands referenceofficial · sync:check fails the build when a generated artifact has drifted.
28npm downloads — openapi-fetchofficial · 5.97M for the week of 5–11 Jul 2026.
29npm downloads — @hey-api/openapi-tsofficial · 2.88M; orval 1.29M; kiota-bundle 47.9k.
30openapi-ts/openapi-typescriptofficial · ⭐ 8.2k · all three packages, one monorepo, shared maintainers.