Kubb vs orval, HeyAPI, and openapi-typescript
Kubb, orval, HeyAPI, and openapi-typescript all generate code from OpenAPI specs. The tables below compare their support, feature by feature. Think a row is wrong? Open an issue or PR on kubb-labs/kubb with the evidence.
Plugin and feature coverage
Legend
- ✅ Built in, no extra config.
- 🟡 Through a third-party or community plugin.
- 🔶 Supported, but needs extra user code.
- 🛑 Not officially supported.
| Feature | Kubb | orval | HeyAPI | openapi-ts |
|---|---|---|---|---|
| OpenAPI 2.0, 3.0, 3.1 input | ✅ | ✅ | ✅ | 🔶1 |
| TypeScript types | ✅ | ✅ | ✅ | ✅ |
| HTTP client (Axios, Fetch) | ✅ | ✅ | ✅ | 🔶2 |
| React Query hooks | ✅ | ✅ | ✅ | 🟡3 |
| Vue Query composables | ✅ | ✅ | ✅ | 🛑 |
| SWR hooks | ✅ | ✅ | ✅ | 🟡4 |
| Zod validation schemas | ✅ | ✅ | ✅5 | 🛑 |
| MSW request handlers | ✅ | ✅ | ✅ | 🛑 |
| Faker.js mock data | ✅ | ✅ | ✅ | 🛑 |
| Cypress E2E tests | ✅ | 🛑 | 🛑 | 🛑 |
| MCP server | ✅ | ✅ | 🛑 | 🛑 |
| Redoc API documentation | ✅ | 🛑 | 🛑 | 🛑 |
| Barrel index files | ✅ | ✅ | ✅ | 🛑 |
Notes
- openapi-typescript reads OpenAPI 3.0 and 3.1 only, so a Swagger 2.0 document has to be up-converted first. Kubb, orval, and HeyAPI accept 2.0 and up-convert it for you.
- openapi-typescript generates types only. The typed client comes from its
openapi-fetchruntime, not per-operation code, andopenapi-fetchis now in maintenance mode. - openapi-typescript generates no hooks. React Query support comes from the first-party
openapi-react-queryruntime, also in maintenance mode. - openapi-typescript relies on the community
swr-openapipackage. - HeyAPI also generates Valibot schemas alongside Zod.
Type safety and response handling
The first table is about which outputs exist. This one is about how well each tool models a single operation and how much of that reaches your code.
On coverage the three are close. The gap is ergonomics. Kubb puts the status on the result, so one switch narrows the body and the same call can throw or return its error. orval does this in its fetch client only. HeyAPI gives a status-keyed type map to index, not a value you branch on at runtime.
The legend matches the table above.
| Feature | Kubb | orval | HeyAPI |
|---|---|---|---|
| Status-discriminated response result | ✅ | 🔶1 | 🔶2 |
| Multiple success (2xx) responses | ✅ | ✅ | ✅ |
| Multiple content types per response | ✅ | ✅ | 🛑3 |
default and wildcard (4XX, 5XX) responses | ✅ | ✅4 | ✅ |
| Typed error responses | ✅ | 🔶5 | ✅ |
| Throw or return the error per call | ✅ | 🛑 | ✅ |
| Zod v4 schemas tied to the types | ✅ | ✅ | ✅ |
| Recursive schemas | ✅ | ✅ | ✅ |
| Server-side schema validation | ✅ | ✅ | ✅ |
Notes
- Only orval's
fetchclient emits a status-narrowable union. Its axios and query clients return the success type and take the error type on the side. - HeyAPI builds a status-keyed type map you index (
Responses[200]), but the SDK returns a flat{ data, error }pair with nostatusto switch on. - On
@hey-api/openapi-tsv0.99.0, a response with bothapplication/jsonandapplication/xmlkeeps only the JSON shape. Kubb and orval emit a variant per content type. - orval expands
4XXand5XXinto unions of concrete codes. Kubb and HeyAPI keep the range key. - orval types the error body in its
fetchclient, or once you wireErrorTypeoroverride.swr.generateErrorTypes. Otherwise it staysError.
openapi-typescript is omitted here. It ships no generated client, so the runtime rows do not apply.
Client runtime
The generated client does more than wrap fetch. It encodes parameters and bodies from the spec, decodes responses by content type, and can validate both ends. See serialization for the full picture.
Two things set Kubb's client apart. It reads each parameter's OpenAPI style and explode from the spec, so query, path, header, and cookie all encode with no config. And codecs register a serialize and deserialize per media type, so XML or YAML round-trips without replacing the client.
The legend matches the tables above.
| Feature | Kubb | orval | HeyAPI |
|---|---|---|---|
| Parameter styles from the spec | ✅ | 🔶1 | 🔶2 |
| Request body serializers (JSON, form-data, urlencoded) | ✅3 | ✅ | ✅ |
| Pluggable codecs per media type (XML, YAML) | ✅ | 🛑4 | 🛑4 |
| Runtime body validation | ✅5 | 🔶5 | ✅5 |
| Server-sent events and streaming | ✅ | 🔶6 | ✅ |
Notes
- Only orval's
fetchclient readsstyleandexplodefrom the spec. Its axios and query clients interpolate path parameters directly and leave query encoding to axios or aqsconfig. - HeyAPI serializes path parameters per parameter but runs one global query serializer, and does not style header or cookie parameters.
- All three encode JSON,
multipart/form-data, andapplication/x-www-form-urlencoded. Kubb also honors the OpenAPIencodingobject, so a form part can set its own content type and style. - orval and HeyAPI expose a single body serializer and one response transformer, so a new media type means replacing them, not registering one.
- Off by default. Kubb validates request and response bodies through any Standard Schema validator (Zod, valibot, arktype). HeyAPI validates both with Zod or Valibot. orval validates responses only, with Zod.
- orval streams NDJSON on its
fetchclient but has no server-sent events (text/event-stream) support. Kubb and HeyAPI consume SSE.
The Kubb toolkit
Every job in Kubb is one of three pieces: an adapter reads the input, a parser renders the output language, and a plugin emits each artifact on the shared AST. When a built-in falls short you write your own, the extension point orval and HeyAPI do not have.
Adapters (input)
@kubb/adapter-oasreads OpenAPI 2.0, 3.0, and 3.1.
Parsers (output)
@kubb/parser-tsrenders TypeScript and TSX.@kubb/parser-mdrenders Markdown.
Plugins (artifacts)
@kubb/plugin-tstypes, interfaces, and enums.@kubb/plugin-zodZod v4 schemas.@kubb/plugin-fetchand@kubb/plugin-axiosHTTP clients.@kubb/plugin-react-query,@kubb/plugin-vue-query, and@kubb/plugin-swrdata-fetching hooks.@kubb/plugin-fakermock data and@kubb/plugin-mswrequest handlers.@kubb/plugin-cypressend-to-end tests.@kubb/plugin-redocAPI documentation.@kubb/plugin-mcpan MCP server for your API.@kubb/plugin-barrelbarrel index files.
What sets Kubb apart
Plugin architecture
Every output is a separate plugin on a shared AST. Kubb parses the spec once, so names stay consistent across outputs and you add only what you need.
Custom adapters and parsers
A custom adapter swaps adapterOas for another input such as AsyncAPI or GraphQL. A custom parser targets another output language such as Python or Rust. orval and HeyAPI have no such extension point: input is OpenAPI and the generators are first-party, so a new format waits on the maintainers, not an adapter or parser you write.
Advanced client
One client backs both fetch and axios. It reads style and explode for query, path, header, and cookie, picks a body encoder from the content type, and decodes the response by media type. codecs add a serialize and deserialize per media type, so XML or YAML round-trips without swapping the client. A call returns a status-discriminated result you narrow with one switch, and throwOnError picks throw or return per call. The codecs and the full parameter-style coverage are what orval and HeyAPI do not match.
Post-enforced plugins
Plugins with enforce: 'post' run after the rest and handle cross-output work like barrel files without touching each plugin. @kubb/plugin-barrel works this way.
Bundler integration
unplugin-kubb runs generation inside Vite, Rollup, Webpack, esbuild, Nuxt, and Astro. HeyAPI ships a Vite plugin and a Nuxt module. orval has no bundler integration.
MCP and AI agents
@kubb/plugin-mcp turns your spec into an MCP server, so an assistant calls your API as typed tools. orval matches this with @orval/mcp. HeyAPI has none. Kubb also ships @kubb/mcp, an MCP server for the generator itself, so Claude, Cursor, or any MCP client runs Kubb from a prompt.
When not to use Kubb
- You use only a few endpoints that rarely change.
- You have no OpenAPI spec and won't write one.
- You need a non-OpenAPI format now and won't write a custom adapter.