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 | ✅ | ✅ | ✅ | ✅ |
| TypeScript types | ✅ | ✅ | ✅ | ✅ |
| HTTP client (Axios, Fetch) | ✅ | ✅ | ✅ | 🔶2 |
| React Query hooks | ✅ | ✅ | ✅ | 🛑 |
| Vue Query composables | ✅ | ✅ | ✅ | 🛑 |
| SWR hooks | ✅ | ✅ | 🛑3 | 🟡4 |
| Zod validation schemas | ✅ | ✅ | ✅1 | 🛑 |
| MSW request handlers | ✅ | ✅ | 🛑 | 🛑 |
| Faker.js mock data | ✅ | 🔶5 | 🛑 | 🛑 |
| Cypress E2E tests | ✅ | 🛑 | 🛑 | 🛑 |
| MCP server | ✅ | 🛑 | 🛑 | 🛑 |
| Redoc API documentation | ✅ | 🛑 | 🛑 | 🛑 |
| Barrel index files | ✅ | 🛑 | 🛑 | 🛑 |
Notes
- HeyAPI also generates Valibot schemas alongside Zod.
- openapi-typescript generates types only. The typed client comes from its
openapi-fetchruntime, not per-operation code, andopenapi-fetchis now in feature freeze. - HeyAPI lists SWR as a roadmap proposal that has not started.
- openapi-typescript relies on the community
swr-openapipackage. - orval has no standalone Faker output. It fills its MSW handlers with
fakerdata instead.
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 and decodes responses by content type, and it 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 correctly with no config. And codecs register a serialize and deserialize per media type, which is how 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 | ✅ | 🛑 | ✅ |
Notes
- orval interpolates path parameters directly and leaves query encoding to axios or a
qsconfig, with no per-parameterstyleorexplode. - 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 covers those plus Ajv, Joi, TypeBox, and Yup. orval validates responses only, with Zod.
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 do neither, so Kubb reaches inputs and outputs they cannot.
Post-enforced plugins
Plugins with enforce: 'post' run after the rest, handling 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 is Vite-only. orval has no bundler integration.
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.