DEV Community

Cover image for Which OpenAPI Codegen Should You Choose? openapi-typescript vs hey-api vs Orval vs Kubb

Which OpenAPI Codegen Should You Choose? openapi-typescript vs hey-api vs Orval vs Kubb

nyaomaru on May 20, 2026

Hoi hoi! I'm @nyaomaru, a frontend engineer who thought OpenAPI codegen would make API clients easier, but somehow ended up lost in a forest of ge...
Collapse
 
codemaster_121482 profile image
Seif Ahmed

Very useful πŸ‘ŒπŸ»

Collapse
 
nyaomaru profile image
nyaomaru

Thx! 😸

Collapse
 
codemaster_121482 profile image
Seif Ahmed

You are welcome 😁!

Collapse
 
hoseinmdev profile image
Hosein Mahmoudi

so useful :))

Collapse
 
nyaomaru profile image
nyaomaru

Thx! 😸

Collapse
 
ergen35 profile image
Serge Eric Hotegni

Amazing article.
Adopted hey-api for about 2 years now!
The only thing that bother me alot is the fact that it turns | null properties into unknown
Ex: if a property type is "string | null", it turns it into "unknow"
Wonder if you have the same error.
Reached out for the maintainer multiple times but to no avail.
Thank you

Collapse
 
nyaomaru profile image
nyaomaru

Thank you for your comment and sharing this!

I checked my generated files again, and in this project I don’t seem to have the same issue.

My schema is OpenAPI 3.0.3 generated by drf-spectacular, and nullable string fields are represented with nullable: true.

For example:

playback_audio_file_path:
  type: string
  nullable: true
Enter fullscreen mode Exit fullscreen mode

is generated as:

playback_audio_file_path?: string | null;
Enter fullscreen mode Exit fullscreen mode

I also checked the generated type file, and string | null appears correctly in many places.

There are some unknown types in my generated output, but they seem to come from different schema shapes, such as additionalProperties, items: {}, or empty schemas like:

processing_json: {}
Enter fullscreen mode Exit fullscreen mode

So at least in my case, string | null itself is not being converted to unknown. 😿

I wonder if the issue may depend on the OpenAPI schema format, for example OpenAPI 3.1 style:

type:
  - string
  - "null"
Enter fullscreen mode Exit fullscreen mode

or maybe a specific combination of $ref, nullable, and the hey-api version difference. πŸ€”

Collapse
 
nyaomaru profile image
nyaomaru

Small update: I also tried the OpenAPI 3.1 style, and it seems to work fine on my side too.

So maybe the issue depends on another condition, such as an OpenAPI schema version older than 3.x, or an older hey-api version than @hey-api/openapi-ts@0.96.1.

At least with my current setup, both OpenAPI 3.0.3 nullable: true and OpenAPI 3.1 type: ["string", "null"] seem to generate string | null correctly. And if you can't generate correctly with same version, it may be a difference of setup config. πŸ€”

Thread Thread
 
ergen35 profile image
Serge Eric Hotegni

Thank you for your answers.
I'm rather using ElysiaJS as my backend framework. It have an openapi plugin that uses Scalar under the hood.
The current openapi version of the generated doc is 3.0.3 style
but there is no $ref in the document.
@hey-api/openapi-ts package is up to date (version 0.97.3)
Maybe i configured it wrong

Thread Thread
 
nyaomaru profile image
nyaomaru

That makes sense. In my case, the OpenAPI schema is generated by drf-spectacular, and it does include many $ref entries under components.schemas.

So I think this may be an important difference:

  • my case: drf-spectacular / Django generates shared schemas with $ref
  • your case: ElysiaJS / Scalar seems to generate inline schemas without $ref πŸ€”

I guess this is probably not controlled by hey-api itself. hey-api is the consumer of the OpenAPI document, while ElysiaJS / Scalar or drf-spectacular is the producer of the OpenAPI document.

No $ref does not automatically mean the schema is broken, but maybe the specific inline nullable shape generated by ElysiaJS is causing hey-api to fall back to unknown.

So my current guess is that this may be related to the OpenAPI output shape from ElysiaJS / Scalar, rather than the hey-api config itself.

Hope this helps a bit! 😺

Thread Thread
 
ergen35 profile image
Serge Eric Hotegni

Yes, it really helps.
I've found out that this feature is still experimental on Elysia framework.
I may have to find other ways to get a proper schema
Thank you very much!

Collapse
 
gunzip_ profile image
D.S.

Thanks for this amazing article!

I was actually looking for a comparison between Orval and Hey-API to share with some colleagues, so this came at the perfect time.

Personally, I think there’s no competition: Hey-API is by far superior to any other OpenAPI client generator I’ve ever tested (and I’ve tried quite a few).

Here are a few thoughts on the points you raised:

"if your project already uses openapi-typescript + openapi-fetch... I do not think you need to migrate immediately"

It's worth noting that, unlike the output generated by Hey-API, this combination doesn't perform any kind of runtime validation on the payload. The client completely trusts the TypeScript types blindly (which, in my opinion, isn't the safest approach, but that's a whole different story...).

"The number of files written to disk becomes a major factor":

That's true, but file I/O bottlenecks can often be mitigated. If you parallelize the execution tasks, you can actually keep the generation speed quite high.

Regarding "Output Size":

We should consider that since a large portion of the output consists of TypeScript types, these are completely stripped out at runtime (e.g., in the final browser bundle). Furthermore, because the generated code repeats the exact same patterns, the gzipped size (which is what the browser actually downloads) doesn't grow linearly with the raw code size. Therefore, a massive raw output size might have a negligible impact on bundle size.

Regarding "Result Union" and Error Models:

A persistent issue that none of the generators I've tested has managed to solve is discriminating between different response schemas. When an endpoint returns a payload that could correspond to multiple success or failure states, these tools don't allow you to easily discriminate between the specific values matching the different schemas defined in the spec. Honestly, this limitation is the unique reason why I ended up writing my own.

Collapse
 
nyaomaru profile image
nyaomaru

Thanks a lot for the detailed comment! 😺

I also took a look at your benchmark repository. It’s interesting, although I’d be careful about drawing strong conclusions from older versions since this space moves quite fast.

For example, I’ve already received feedback that newer versions of hey-api have improved generation speed quite a bit, and that the output structure can be customized with options like getFilePath. Things are moving really fast.

The client completely trusts the TypeScript types blindly

Absolutely, TypeScript types alone are not runtime validation. I agree that we still need to think carefully about runtime validation, especially at API boundaries.

Therefore, a massive raw output size might have a negligible impact on bundle size.

I agree with your point that raw output size and actual runtime/bundle impact should be considered separately. Type-only output and gzip can make the practical impact very different from the raw generated size.

And the response schema discrimination issue is also a really interesting point. I didn’t cover it deeply in this article, but I can see why that could become a reason to build a custom solution.

Thanks again, your comment gives me several good points to revisit. πŸ‘

Collapse
 
harjjotsinghh profile image
Harjot Singh

i totally get what you mean about getting lost in the generated files. it's crucial that the output is manageable and easy to work with. at moonshift, you can quickly build a full next.js app with postgres and auth deployed in around 7 minutes. you have complete control over the code on your github. let me know if you're interested in a free run to see how it goes.

Collapse
 
nyaomaru profile image
nyaomaru

For small projects, that kind of workflow sounds very useful. But once the project becomes a large business system, generated files can easily become a forest... 😿🌳

Is Moonshift more like a boilerplate application generator?

I think it could become even more interesting if it had a strong integration story with OpenAPI codegen. Passing types from the server side to the frontend in a loosely coupled way is still a hard problem, especially in larger systems.

So if Moonshift can help with that boundary, I think there may be real demand for it. 😺