<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged openapi</title>
<link>https://coderlegion.com/tag/openapi</link>
<description>Powered by Question2Answer</description>
<item>
<title>Adding OpenAPI Support to Mummy, a Nim HTTP Framework</title>
<link>https://coderlegion.com/27091/adding-openapi-support-to-mummy-a-nim-http-framework</link>
<description>&lt;h2 id=&quot;adding-openapi-support-to-mummy-lessons-from-extending-nim-s-thread-per-request-http-server&quot;&gt;Adding OpenAPI Support to Mummy: Lessons from Extending Nim's Thread-Per-Request HTTP Server&lt;/h2&gt;
&lt;p&gt;Mummy is a multi-threaded HTTP 1.1 and WebSocket server for Nim built around a simple, unapologetic idea: skip &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; entirely and let a pool of worker threads handle requests the old-fashioned way — one thread, one request, blocking calls allowed. No &lt;code&gt;Future[T]&lt;/code&gt;, no colored functions, no async stack traces to untangle. For a systems language with real threads and ARC/ORC to make that cheap, it's a compelling trade.&lt;/p&gt;
&lt;p&gt;What Mummy doesn't ship with, though, is any notion of API documentation. There's no built-in way to describe your routes, generate a schema, or hand a frontend team (or your future self) a &lt;code&gt;/docs&lt;/code&gt; page. That gap is what got me building.&lt;/p&gt;
&lt;h3 id=&quot;why-this-gap-matters&quot;&gt;Why This Gap Matters&lt;/h3&gt;
&lt;p&gt;Nim's web ecosystem is small enough that most frameworks are optimized for the author's own use case first. Mummy is optimized for raw throughput and simplicity — and it succeeds at both. But once an API has more than a couple of routes, &quot;read the source to find out what a request body looks like&quot; stops scaling. OpenAPI (formerly Swagger) solves this by generating a machine-readable schema of your API directly from your code, which then powers interactive docs, client SDK generation, and request validation.&lt;/p&gt;
&lt;p&gt;I forked Mummy and built this in as a layer on top, rather than a separate tool that has to be kept in sync by hand.&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;The fork adds two core modules — &lt;code&gt;openapi_schema.nim&lt;/code&gt; and &lt;code&gt;openapi_router.nim&lt;/code&gt; — plus an &lt;code&gt;OpenApiRouter&lt;/code&gt; that wraps Mummy's normal routing with metadata: summaries, tags, and response schemas generated via &lt;code&gt;schemaOf&lt;/code&gt;. Every one of the 12 bundled examples now serves a live &lt;code&gt;/openapi.json&lt;/code&gt; and an interactive &lt;code&gt;/docs&lt;/code&gt; page, generated from the same route definitions that handle requests — no separate spec file to maintain.&lt;/p&gt;
&lt;p&gt;From there I added three more pieces that came up naturally once the schema layer existed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Typed path params&lt;/strong&gt; (&lt;code&gt;openapi_params.nim&lt;/code&gt;) — &lt;code&gt;pathParam[T](request, &quot;id&quot;)&lt;/code&gt; pulls a path segment straight out as an &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, or whatever type you declare, instead of manually parsing strings on every handler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request body validation&lt;/strong&gt; (&lt;code&gt;openapi_validation.nim&lt;/code&gt;) — &lt;code&gt;parseValidatedBody[T]&lt;/code&gt; checks an incoming JSON body against the same schema that &lt;code&gt;schemaOf&lt;/code&gt; generated for your docs, so the validation and the documentation can't drift apart.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable middleware&lt;/strong&gt; (&lt;code&gt;openapi_middleware.nim&lt;/code&gt;) — a &lt;code&gt;Middleware&lt;/code&gt; type and a &lt;code&gt;use()&lt;/code&gt; method on the router, with logging and bearer-auth middleware included out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every feature was compiled and exercised with real HTTP requests, not just &lt;code&gt;nim check&lt;/code&gt;, before I called it done — including converting a wildcard WebSocket route to a named path param and confirming it fixed a small latent bug where the old code read the raw request URI (query string and all) instead of the decoded path.&lt;/p&gt;
&lt;h3 id=&quot;what-the-wider-nim-community-s-experience-with-mummy-adds&quot;&gt;What the Wider Nim Community's Experience With Mummy Adds&lt;/h3&gt;
&lt;p&gt;Reading through discussion around Mummy on the Nim mailing list and forum was useful context while building this. One recurring theme: Mummy's design intentionally keeps some things out of scope in the name of simplicity. A developer comparing thread-per-request servers noted that Mummy's multipart handling, while present, buffers the entire upload into memory rather than streaming it — a deliberate trade-off for simplicity that matters if you're building something like a file-upload service, and pointed to alternatives like Guildenstern for cases needing streaming multipart support.&lt;/p&gt;
&lt;p&gt;That trade-off is directly relevant to schema-driven validation work: &lt;code&gt;parseValidatedBody[T]&lt;/code&gt; inherits the same &quot;load it all, then check it&quot; model. For typical JSON APIs that's a non-issue, but it's a useful thing to know going in if you're planning a Mummy-based service that also needs large file uploads — you may want to keep that path outside the validated-body flow rather than fight the framework's grain.&lt;/p&gt;
&lt;p&gt;It's also worth knowing you're not alone in wanting typed, schema-aware APIs on Mummy — a separate project, Sarcophagus, tackles a similar problem from a different angle: typed request/response encoding across JSON, CBOR, and MessagePack, plus its own Swagger/OpenAPI generation and OAuth2 helpers. Different design choices, same underlying itch in the ecosystem — which suggests this is a real gap worth filling rather than a one-off preference of mine.&lt;/p&gt;
&lt;h3 id=&quot;putting-it-to-use&quot;&gt;Putting It to Use&lt;/h3&gt;
&lt;p&gt;To validate the fork beyond the bundled examples, I built a small Todo API demo using the typed params, validation, and middleware together, and I'm deploying it live on Render as a working reference — a REST API with real request validation and generated docs, running on a threaded model with no &lt;code&gt;async&lt;/code&gt; in sight.&lt;/p&gt;
&lt;h3 id=&quot;where-this-goes-next&quot;&gt;Where This Goes Next&lt;/h3&gt;
&lt;p&gt;The fork is usable today for anyone building JSON APIs on Mummy who wants documentation and validation that can't silently drift from the actual routes. If you're working in the Nim web space and hit the same &quot;no schema, no docs&quot; wall, I'd genuinely like to hear how you're solving it — feel free to reach out.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27091/adding-openapi-support-to-mummy-a-nim-http-framework</guid>
<pubDate>Sun, 13 Sep 2026 22:28:03 +0000</pubDate>
</item>
<item>
<title>Which OpenAPI Codegen Should You Choose? openapi-typescript vs hey-api vs Orval vs Kubb</title>
<link>https://coderlegion.com/24486/which-openapi-codegen-should-you-choose-openapi-typescript-vs-hey-api-vs-orval-vs-kubb</link>
<description>&lt;p&gt;Hoi hoi!&lt;/p&gt;
&lt;p&gt;I'm &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru&quot;&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who thought OpenAPI codegen would make API clients easier, but somehow ended up lost in a forest of generated files. &lt;/p&gt;
&lt;p&gt;Writing API clients by hand is painful.&lt;/p&gt;
&lt;p&gt;Usually, we want things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generated TypeScript types&lt;/li&gt;
&lt;li&gt;generated request functions&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;TanStack Query&lt;/code&gt; hooks&lt;/li&gt;
&lt;li&gt;maybe &lt;code&gt;MSW&lt;/code&gt; mocks&lt;/li&gt;
&lt;li&gt;maybe even &lt;code&gt;Zod&lt;/code&gt; schemas&lt;/li&gt;
&lt;li&gt;and, if possible, everything growing automatically from OpenAPI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is where OpenAPI codegen tools are useful.&lt;/p&gt;
&lt;p&gt;But after comparing several tools with a fairly large real-world schema, I realized something important:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenAPI codegen is not only about what a tool can generate.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The generated output itself also matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how many files it creates&lt;/li&gt;
&lt;li&gt;how fast generation is&lt;/li&gt;
&lt;li&gt;how friendly it is to linting&lt;/li&gt;
&lt;li&gt;how easy it is for the IDE to index&lt;/li&gt;
&lt;li&gt;how errors are modeled&lt;/li&gt;
&lt;li&gt;how authentication is handled&lt;/li&gt;
&lt;li&gt;how much generated code your team is willing to maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this article, I compared the following four tools:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                  &lt;/th&gt;&lt;th&gt;Version tested &lt;/th&gt;&lt;th&gt;Main setup&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;  &lt;/td&gt;&lt;td&gt;       v7.13.0 &lt;/td&gt;&lt;td&gt;types only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;       v0.96.1 &lt;/td&gt;&lt;td&gt;types + SDK + fetch client&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;        v8.8.1 &lt;/td&gt;&lt;td&gt;&lt;code&gt;mode: &quot;tags-split&quot;&lt;/code&gt; / &lt;code&gt;client: &quot;fetch&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;                &lt;/td&gt;&lt;td&gt;       v4.37.2 &lt;/td&gt;&lt;td&gt;client / types / schemas generated by plugins&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rr61d7ozn3dwxht4s5jo.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;The OpenAPI schema used for this comparison was roughly this size. (It was generated by &lt;code&gt;drf-spectacular&lt;/code&gt;)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Item                 &lt;/th&gt;&lt;th&gt;       Value&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi.yaml&lt;/code&gt; lines &lt;/td&gt;&lt;td&gt;about 75,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi.yaml&lt;/code&gt; size  &lt;/td&gt;&lt;td&gt;  about 2 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;operations           &lt;/td&gt;&lt;td&gt; about 1,200&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;Note: Some endpoint names, schema names, type names, and directory names in this article are simplified or replaced for publication. Also, these libraries move fast, so the generated output and issues described here are based on the versions tested at the time of writing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a small API, honestly, any of these tools may work fine.&lt;/p&gt;
&lt;p&gt;But at this scale, differences become very visible in generation speed, file count, lint behavior, enum handling, error models, and day-to-day maintainability.&lt;/p&gt;
&lt;p&gt;Let's walk through the comparison.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;My conclusion is this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation                                                                  &lt;/th&gt;&lt;th&gt;Tool I would choose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;You only need TypeScript types                                             &lt;/td&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;You want operationId-based SDK functions and interceptors                  &lt;/td&gt;&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;You want &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, &lt;code&gt;MSW&lt;/code&gt;, or &lt;code&gt;Faker&lt;/code&gt; generated from OpenAPI &lt;/td&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;You value 1 operation = 1 file and plugin-based codegen architecture       &lt;/td&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For my use case, &lt;strong&gt;hey-api felt the most practical&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The reasons were simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it generates functions from &lt;code&gt;operationId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;arguments are consistently shaped like &lt;code&gt;{ query, path, body }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;it has a result-style error model&lt;/li&gt;
&lt;li&gt;it supports axios-like interceptors&lt;/li&gt;
&lt;li&gt;the generated output is much lighter than Orval or Kubb&lt;/li&gt;
&lt;li&gt;it still leaves room for &lt;code&gt;TanStack Query&lt;/code&gt; and &lt;code&gt;Zod&lt;/code&gt; plugins later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, if your project already uses &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt; and you are happy with it, I do not think you need to migrate immediately.&lt;/p&gt;
&lt;p&gt;The difference is not huge enough to justify migration by itself.&lt;/p&gt;
&lt;p&gt;I would consider switching only when you start feeling pain around SDK functions, interceptors, result handling, mocks, generated hooks, or schema-driven tooling.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In this article, I will call &lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; simply &lt;code&gt;hey-api&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;two-families-of-openapi-codegen-tools&quot;&gt;Two families of OpenAPI codegen tools&lt;/h3&gt;
&lt;p&gt;Before comparing each tool one by one, I think it is easier to divide them into two families.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Axis                 &lt;/th&gt;&lt;th&gt;Family A: thin output, client-focused      &lt;/th&gt;&lt;th&gt;Family B: SDK-first, ecosystem generation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Representative tools &lt;/td&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; / &lt;code&gt;hey-api&lt;/code&gt;           &lt;/td&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt; / &lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Main purpose         &lt;/td&gt;&lt;td&gt;accurate types and lightweight runtime     &lt;/td&gt;&lt;td&gt;generated SDK + hooks + mocks + validation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error model          &lt;/td&gt;&lt;td&gt;result-style &lt;code&gt;{ data, error, response }&lt;/code&gt;   &lt;/td&gt;&lt;td&gt;wrapper / throw / client-dependent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Output size          &lt;/td&gt;&lt;td&gt;small: types only or compact SDK           &lt;/td&gt;&lt;td&gt;large: tag-level or operation-level files&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Ecosystem features   &lt;/td&gt;&lt;td&gt;&lt;code&gt;TanStack Query&lt;/code&gt; / &lt;code&gt;Zod&lt;/code&gt; depending on tool &lt;/td&gt;&lt;td&gt;&lt;code&gt;TanStack Query&lt;/code&gt; / &lt;code&gt;Zod&lt;/code&gt; / &lt;code&gt;MSW&lt;/code&gt; / &lt;code&gt;Faker&lt;/code&gt; / &lt;code&gt;SWR&lt;/code&gt; etc.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Configuration style  &lt;/td&gt;&lt;td&gt;minimal                                    &lt;/td&gt;&lt;td&gt;codegen platform&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This distinction is important.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; and &lt;code&gt;Kubb&lt;/code&gt; are both OpenAPI codegen tools, but they are not trying to solve the same problem.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; mainly focuses on generating accurate TypeScript types.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt;, on the other hand, is closer to a codegen platform that can generate clients, types, schemas, mocks, and other assets in a structured way.&lt;/p&gt;
&lt;p&gt;Comparing them directly is a bit like comparing a kitchen knife with a full kitchen. ‍&lt;/p&gt;
&lt;p&gt;Both are related to cooking, but they are not the same kind of thing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-generation-speed&quot;&gt;️ Generation speed&lt;/h3&gt;
&lt;p&gt;First, generation speed.&lt;/p&gt;
&lt;p&gt;I measured each command three times on &lt;code&gt;WSL2&lt;/code&gt; / &lt;code&gt;Node v24.14&lt;/code&gt; using &lt;code&gt;npx &amp;lt;cmd&amp;gt;&lt;/code&gt;. So the numbers include &lt;code&gt;npx&lt;/code&gt; startup overhead. (a custom-built PC)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                  &lt;/th&gt;&lt;th&gt;1st run &lt;/th&gt;&lt;th&gt;2nd run &lt;/th&gt;&lt;th&gt;3rd run &lt;/th&gt;&lt;th&gt;         Average &lt;/th&gt;&lt;th&gt;     Max RSS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;  &lt;/td&gt;&lt;td&gt; 1.66 s &lt;/td&gt;&lt;td&gt; 1.50 s &lt;/td&gt;&lt;td&gt; 1.40 s &lt;/td&gt;&lt;td&gt; &lt;strong&gt;about 1.5 s&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;about 335 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt; 8.74 s &lt;/td&gt;&lt;td&gt; 8.84 s &lt;/td&gt;&lt;td&gt; 6.43 s &lt;/td&gt;&lt;td&gt; &lt;strong&gt;about 8.0 s&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;about 495 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;               &lt;/td&gt;&lt;td&gt; 6.01 s &lt;/td&gt;&lt;td&gt; 6.12 s &lt;/td&gt;&lt;td&gt; 4.48 s &lt;/td&gt;&lt;td&gt; &lt;strong&gt;about 5.5 s&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;about 415 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;                &lt;/td&gt;&lt;td&gt;17.29 s &lt;/td&gt;&lt;td&gt;19.82 s &lt;/td&gt;&lt;td&gt;17.13 s &lt;/td&gt;&lt;td&gt;&lt;strong&gt;about 18.1 s&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;about 880 MB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Relative to &lt;code&gt;openapi-typescript&lt;/code&gt;, the difference looked like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool      &lt;/th&gt;&lt;th&gt;   Relative speed&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;   &lt;/td&gt;&lt;td&gt;about 3.7x slower&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt; &lt;/td&gt;&lt;td&gt;about 5.3x slower&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;    &lt;/td&gt;&lt;td&gt; about 12x slower&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;why-openapi-typescript-is-so-fast&quot;&gt;Why openapi-typescript is so fast&lt;/h4&gt;
&lt;p&gt;In this setup, &lt;code&gt;openapi-typescript&lt;/code&gt; only generates types into a single file.&lt;/p&gt;
&lt;p&gt;The workflow is basically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;read the schema&lt;/li&gt;
&lt;li&gt;convert it into TypeScript types&lt;/li&gt;
&lt;li&gt;write one file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That means very little file I/O.&lt;/p&gt;
&lt;p&gt;The generated output was just &lt;code&gt;types.gen.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At about 1.5 seconds, it is fast enough that running it in CI, pre-commit, or during local development barely feels like a problem.&lt;/p&gt;
&lt;h4 id=&quot;why-kubb-is-slower&quot;&gt;Why Kubb is slower&lt;/h4&gt;
&lt;p&gt;Kubb has a different philosophy.&lt;/p&gt;
&lt;p&gt;It splits things much more aggressively.&lt;/p&gt;
&lt;p&gt;In this test, I generated operation-level clients, operation-level types, and schemas.&lt;/p&gt;
&lt;p&gt;So the cost is not only computation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The number of files written to disk becomes a major factor.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The structure is clean from a bundling and organization perspective, but with around 1,200 operations, the file system cost becomes noticeable.&lt;/p&gt;
&lt;p&gt;For this kind of schema, I would not treat Kubb as something I casually run on every small change unless the generation target is narrowed.&lt;/p&gt;
&lt;p&gt;It feels more suitable for a workflow where generated files are committed, generated in CI, or carefully scoped.&lt;/p&gt;
&lt;h4 id=&quot;orval-was-faster-than-i-expected&quot;&gt;Orval was faster than I expected&lt;/h4&gt;
&lt;p&gt;I expected Orval to be slower.&lt;/p&gt;
&lt;p&gt;It can generate things like &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, and &lt;code&gt;MSW&lt;/code&gt;, so I assumed it would be heavier.&lt;/p&gt;
&lt;p&gt;But with &lt;code&gt;tags-split&lt;/code&gt;, the main functions are grouped by tag.&lt;/p&gt;
&lt;p&gt;In my schema, there were about 25 tags.&lt;/p&gt;
&lt;p&gt;So Orval was not generating one giant file, but it also was not generating one file per operation.&lt;/p&gt;
&lt;p&gt;That middle ground worked well for speed.&lt;/p&gt;
&lt;p&gt;However, this does not mean the output was small.&lt;/p&gt;
&lt;p&gt;It still generated more than 2,600 schema files.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-output-size&quot;&gt;️ Output size&lt;/h3&gt;
&lt;p&gt;Next, generated output size and file count.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                  &lt;/th&gt;&lt;th&gt;Output                 &lt;/th&gt;&lt;th&gt;File count &lt;/th&gt;&lt;th&gt;Total size&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;  &lt;/td&gt;&lt;td&gt;&lt;code&gt;src/api/types.gen.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;         1 &lt;/td&gt;&lt;td&gt;    2.4 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;src/api/hey-api/&lt;/code&gt;     &lt;/td&gt;&lt;td&gt;        16 &lt;/td&gt;&lt;td&gt;    2.9 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;&lt;code&gt;src/api/orval/&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;     2,719 &lt;/td&gt;&lt;td&gt;     14 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;                &lt;/td&gt;&lt;td&gt;&lt;code&gt;src/api/kubb/&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;     3,877 &lt;/td&gt;&lt;td&gt;     24 MB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Just from these numbers, the tools clearly live in different worlds.&lt;/p&gt;
&lt;p&gt;My rough feeling was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;: one house &lt;/li&gt;
&lt;li&gt;&lt;code&gt;hey-api&lt;/code&gt;: a small town ️&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Orval&lt;/code&gt;: a small forest &lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kubb&lt;/code&gt;: a deep forest &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BTW, I love rural life so forest is better. &lt;/p&gt;
&lt;h4 id=&quot;openapi-typescript&quot;&gt;openapi-typescript&lt;/h4&gt;
&lt;p&gt;The output was only &lt;code&gt;types.gen.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It contained:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;paths&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operations&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One file is simple and easy to understand.&lt;/p&gt;
&lt;p&gt;But a 2.4 MB TypeScript file can still be uncomfortable for IDE indexing.&lt;/p&gt;
&lt;p&gt;So even though the file count is tiny, the single file itself is large.&lt;/p&gt;
&lt;h4 id=&quot;hey-api&quot;&gt;hey-api&lt;/h4&gt;
&lt;p&gt;hey-api generated 16 files.&lt;/p&gt;
&lt;p&gt;The main files looked like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;File            &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;types.gen.ts&lt;/code&gt;  &lt;/td&gt;&lt;td&gt;request / response types&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sdk.gen.ts&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;generated operation functions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;barrel exports&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client.gen.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;default client instance&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client/&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;fetch client implementation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;core/&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;auth, serializers, utils, etc.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;types.gen.ts&lt;/code&gt; was about 1.7 MB, and &lt;code&gt;sdk.gen.ts&lt;/code&gt; was about 750 KB.&lt;/p&gt;
&lt;p&gt;With 1,200 operations, the SDK file naturally becomes large.&lt;/p&gt;
&lt;p&gt;But compared with Orval and Kubb, the output still felt manageable.&lt;/p&gt;
&lt;h4 id=&quot;orval&quot;&gt;Orval&lt;/h4&gt;
&lt;p&gt;I used Orval with &lt;code&gt;tags-split&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The structure looked roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/api/orval/
├─ schemas/
│  ├─ ordersWriteRequest.ts
│  ├─ ordersListParams.ts
│  └─ ...
├─ order/
│  └─ order.ts
├─ product/
│  └─ product.ts
└─ ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Functions were grouped by tag.&lt;/p&gt;
&lt;p&gt;In this schema:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tag files: about 25&lt;/li&gt;
&lt;li&gt;schema files: more than 2,600&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The largest generated file was around 30,000 lines.&lt;/p&gt;
&lt;h4 id=&quot;kubb&quot;&gt;Kubb&lt;/h4&gt;
&lt;p&gt;Kubb generated an even more split structure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/api/kubb/
├─ types/
├─ clients/
├─ schemas/
└─ index.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Directory  &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;types/&lt;/code&gt;   &lt;/td&gt;&lt;td&gt;operation-level types&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;clients/&lt;/code&gt; &lt;/td&gt;&lt;td&gt;operation-level functions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;schemas/&lt;/code&gt; &lt;/td&gt;&lt;td&gt;individual schemas&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;index.ts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;barrel exports&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The nice part is that the structure is close to 1 operation = 1 file.&lt;/p&gt;
&lt;p&gt;That makes grep easy and can be friendly to tree-shaking.&lt;/p&gt;
&lt;p&gt;But the tradeoff is real:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;large PR diffs&lt;/li&gt;
&lt;li&gt;heavier IDE indexing&lt;/li&gt;
&lt;li&gt;slower clone and checkout&lt;/li&gt;
&lt;li&gt;more generated files to commit or ignore&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;calling-the-same-endpoint&quot;&gt;Calling the same endpoint&lt;/h3&gt;
&lt;p&gt;Now let's compare the actual call sites.&lt;/p&gt;
&lt;p&gt;Assume this endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;GET /orders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It accepts common list query parameters like &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;page_size&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;openapi-typescript-1&quot;&gt;openapi-typescript&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; only generates types, so I used it with &lt;code&gt;openapi-fetch&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import createClient from &quot;openapi-fetch&quot;;
import type { paths } from &quot;./api/types.gen&quot;;

const client = createClient&amp;lt;paths&amp;gt;({
  baseUrl: &quot;http://localhost:8000&quot;,
});

const { data, response } = await client.GET(&quot;/orders&quot;, {
  params: {
    query: {
      page: 1,
      page_size: 20,
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is that the endpoint is specified as a path string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;client.GET(&quot;/orders&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is simple and readable.&lt;/p&gt;
&lt;p&gt;IDE autocomplete also works well.&lt;/p&gt;
&lt;p&gt;But it is not operationId-based, so if the endpoint changes, rename refactoring is not as direct as function-based clients.&lt;/p&gt;
&lt;h4 id=&quot;hey-api-1&quot;&gt;hey-api&lt;/h4&gt;
&lt;p&gt;hey-api generates SDK functions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { ordersList } from &quot;./api/hey-api/sdk.gen&quot;;

const { data, response } = await ordersList({
  query: {
    page: 1,
    page_size: 20,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function name comes from &lt;code&gt;operationId&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I like this because you can grep by function name and use IDE rename refactoring more naturally.&lt;/p&gt;
&lt;p&gt;The argument shape is also consistent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{
  query,
  path,
  body,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This consistency becomes useful later when building wrappers, logging, retries, auth handling, or tracing.&lt;/p&gt;
&lt;h4 id=&quot;orval-1&quot;&gt;Orval&lt;/h4&gt;
&lt;p&gt;For Orval, I used &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;tags-split&lt;/code&gt;, and &lt;code&gt;useNamedParameters: true&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { ordersList } from &quot;./api/orval/orders/order&quot;;

const { data, status, headers } = await ordersList({
  page: 1,
  page_size: 20,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The return value is like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{
  data,
  status,
  headers,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, this is easy to use.&lt;/p&gt;
&lt;p&gt;But with the default fetch client, 4xx and 5xx responses are not thrown as errors.&lt;/p&gt;
&lt;p&gt;That point is very important in real applications.&lt;/p&gt;
&lt;h4 id=&quot;kubb-1&quot;&gt;Kubb&lt;/h4&gt;
&lt;p&gt;Kubb generates operation-level functions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { ordersList } from &quot;./api/kubb/clients/ordersList&quot;;

const data = await ordersList({
  page: 1,
  page_size: 20,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On success, it returns the response data directly.&lt;/p&gt;
&lt;p&gt;So the call site is very short.&lt;/p&gt;
&lt;p&gt;However, if you want to access response headers or status, the default return shape may not be enough.&lt;/p&gt;
&lt;p&gt;Also, path parameters and body arguments can become positional depending on the endpoint, which can make generic wrappers harder to design.&lt;/p&gt;
&lt;h3 id=&quot;-path-params-and-body-arguments&quot;&gt;️ Path params and body arguments&lt;/h3&gt;
&lt;p&gt;A list endpoint does not show all differences, so let's compare path params and body arguments too.&lt;/p&gt;
&lt;h4 id=&quot;get-by-id&quot;&gt;GET by id&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// openapi-typescript
const { data } = await client.GET(&quot;/orders/{id}&quot;, {
  params: {
    path: {
      id: &quot;123&quot;,
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// hey-api
const { data } = await ordersRetrieve({
  path: {
    id: &quot;123&quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Orval
const { data } = await ordersRetrieve({
  id: &quot;123&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Kubb
const data = await ordersRetrieve(&quot;123&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Summary:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                 &lt;/th&gt;&lt;th&gt;Path param style&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ path: { id } }&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;              &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ id }&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;positional argument like &lt;code&gt;&quot;123&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Kubb is short.&lt;/p&gt;
&lt;p&gt;But if an endpoint has multiple path params, positional arguments grow like &lt;code&gt;(id1, id2, ...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is partly a preference issue, but for large API clients I usually prefer named object arguments.&lt;/p&gt;
&lt;h4 id=&quot;post-body&quot;&gt;POST body&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// openapi-typescript
await client.POST(&quot;/orders&quot;, {
  body,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// hey-api
await ordersCreate({
  body,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Orval
await ordersCreate(body);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Kubb
await ordersCreate(body);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Orval and Kubb feel natural when the body is the first argument.&lt;/p&gt;
&lt;p&gt;But for POST endpoints without a body, this can sometimes lead to explicit &lt;code&gt;undefined&lt;/code&gt; arguments.&lt;/p&gt;
&lt;p&gt;hey-api and openapi-typescript add one level with &lt;code&gt;{ body }&lt;/code&gt;, but the shape remains consistent with &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a large application, I personally prefer the consistency of hey-api.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;function-signature-philosophy&quot;&gt;Function signature philosophy&lt;/h3&gt;
&lt;p&gt;Here is the broader comparison of call signatures.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Axis                   &lt;/th&gt;&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;             &lt;/th&gt;&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt;                &lt;/th&gt;&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;endpoint selection     &lt;/td&gt;&lt;td&gt;URL string           &lt;/td&gt;&lt;td&gt;operationId function  &lt;/td&gt;&lt;td&gt;operationId function   &lt;/td&gt;&lt;td&gt;operationId function&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;query                  &lt;/td&gt;&lt;td&gt;&lt;code&gt;params.query&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ query }&lt;/code&gt;           &lt;/td&gt;&lt;td&gt;first argument         &lt;/td&gt;&lt;td&gt;first argument&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;path                   &lt;/td&gt;&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ path }&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ id }&lt;/code&gt; or positional &lt;/td&gt;&lt;td&gt;positional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;body                   &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ body }&lt;/code&gt;           &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ body }&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;first argument         &lt;/td&gt;&lt;td&gt;first argument&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;headers                &lt;/td&gt;&lt;td&gt;options / middleware &lt;/td&gt;&lt;td&gt;options / interceptor &lt;/td&gt;&lt;td&gt;&lt;code&gt;RequestInit&lt;/code&gt;          &lt;/td&gt;&lt;td&gt;config&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;cross-cutting behavior &lt;/td&gt;&lt;td&gt;middleware           &lt;/td&gt;&lt;td&gt;interceptor           &lt;/td&gt;&lt;td&gt;mutator                &lt;/td&gt;&lt;td&gt;config / custom client&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key strength of hey-api is that every endpoint moves toward the same shape:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await api({
  query,
  path,
  body,
  headers,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That makes cross-cutting behavior easier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;trace ids&lt;/li&gt;
&lt;li&gt;request ids&lt;/li&gt;
&lt;li&gt;custom headers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Orval and Kubb often produce shorter call sites, but signatures can vary more by endpoint.&lt;/p&gt;
&lt;p&gt;In a small codebase, that is probably fine.&lt;/p&gt;
&lt;p&gt;In a 1,200-operation API, consistency matters a lot more.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-error-model&quot;&gt;⚠️ Error model&lt;/h3&gt;
&lt;p&gt;This was one of the most important differences.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                                   &lt;/th&gt;&lt;th&gt;Default behavior                         &lt;/th&gt;&lt;th&gt;Can be changed?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; + &lt;code&gt;openapi-fetch&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ data, error, response }&lt;/code&gt; result union &lt;/td&gt;&lt;td&gt;can throw via middleware&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;                              &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ data, error, response }&lt;/code&gt; result union &lt;/td&gt;&lt;td&gt;can throw with &lt;code&gt;throwOnError: true&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;                                &lt;/td&gt;&lt;td&gt;wrapper like &lt;code&gt;{ data, status, headers }&lt;/code&gt; &lt;/td&gt;&lt;td&gt;can throw with custom fetch / mutator / config&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;                                 &lt;/td&gt;&lt;td&gt;success returns &lt;code&gt;data&lt;/code&gt;, failure throws   &lt;/td&gt;&lt;td&gt;customizable via client&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;result-union-in-openapi-typescript-and-hey-api&quot;&gt;Result union in openapi-typescript and hey-api&lt;/h4&gt;
&lt;p&gt;With &lt;code&gt;openapi-fetch&lt;/code&gt; and hey-api, non-2xx responses can be represented in &lt;code&gt;error&lt;/code&gt; if the OpenAPI schema defines them.&lt;/p&gt;
&lt;p&gt;But in my &lt;code&gt;drf-spectacular&lt;/code&gt; schema, most non-2xx responses were not described.&lt;/p&gt;
&lt;p&gt;So in practice, &lt;code&gt;error&lt;/code&gt; was often close to &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That means I still need a runtime check like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { data, response } = await client.GET(&quot;/orders&quot;, {
  params: {
    query: {
      page: 1,
    },
  },
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

return data;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With hey-api, I can also opt into throwing behavior per call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { data } = await ordersList({
  query: {
    page: 1,
  },
  throwOnError: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I like having both options.&lt;/p&gt;
&lt;p&gt;Sometimes I want a result-style response.&lt;/p&gt;
&lt;p&gt;Sometimes I want HTTP errors to throw immediately.&lt;/p&gt;
&lt;h4 id=&quot;orval-s-default-fetch-behavior-needs-attention&quot;&gt;Orval's default fetch behavior needs attention&lt;/h4&gt;
&lt;p&gt;With Orval's default fetch client, 4xx and 5xx responses do not throw automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const res = await ordersList({
  page: 1,
});

if (res.status !== 200) {
  throw new Error(`HTTP ${res.status}`);
}

return res.data;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the team forgets to check &lt;code&gt;status&lt;/code&gt;, it is possible to treat an error response as if it were success data.&lt;/p&gt;
&lt;p&gt;For example, the code may try to access &lt;code&gt;data.results&lt;/code&gt; and then fail at runtime because the actual response body was an error shape.&lt;/p&gt;
&lt;p&gt;This is easy to miss if you only look at TypeScript types.&lt;/p&gt;
&lt;p&gt;If I used Orval in production, I would choose one of these patterns:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;always check &lt;code&gt;status&lt;/code&gt; at the call site&lt;/li&gt;
&lt;li&gt;configure the fetch client with &lt;code&gt;override.fetch.forceSuccessResponse: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;override.mutator&lt;/code&gt; and write a custom fetch wrapper for auth, refresh, and errors&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A custom fetch may look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// custom-fetch.ts
export const customFetch = async &amp;lt;T&amp;gt;(
  url: string,
  options: RequestInit,
): Promise&amp;lt;T&amp;gt; =&amp;gt; {
  const res = await fetch(url, options);

  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }

  return res.json();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Orval is powerful, but this behavior should be made explicit as a team rule.&lt;/p&gt;
&lt;h4 id=&quot;kubb-s-throw-model&quot;&gt;Kubb's throw model&lt;/h4&gt;
&lt;p&gt;Kubb is closer to a throw-based model by default.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const data = await ordersList({
  page: 1,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On success, you get the data directly.&lt;/p&gt;
&lt;p&gt;On failure, it throws.&lt;/p&gt;
&lt;p&gt;This is pleasant in UI code.&lt;/p&gt;
&lt;p&gt;But if you need response metadata, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Retry-After&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;rate limit headers&lt;/li&gt;
&lt;li&gt;pagination headers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;then the default return value may not be enough, and you may need a custom client or wrapper.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-authentication-and-interceptors&quot;&gt;✅ Authentication and interceptors&lt;/h3&gt;
&lt;p&gt;Authentication handling also differs quite a bit.&lt;/p&gt;
&lt;h4 id=&quot;openapi-fetch&quot;&gt;openapi-fetch&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;openapi-fetch&lt;/code&gt; supports middleware.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;client.use({
  onRequest: ({ request }) =&amp;gt; {
    request.headers.set(&quot;Authorization&quot;, `Bearer ${getToken()}`);
    return request;
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you already use &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt;, this is clean and lightweight.&lt;/p&gt;
&lt;h4 id=&quot;hey-api-2&quot;&gt;hey-api&lt;/h4&gt;
&lt;p&gt;hey-api has axios-like interceptors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;client.interceptors.request.use((request) =&amp;gt; {
  request.headers.set(&quot;Authorization&quot;, `Bearer ${getToken()}`);
  return request;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This API feels familiar.&lt;/p&gt;
&lt;p&gt;For common headers, logging, trace ids, and 401 refresh flows, this style is easy to work with.&lt;/p&gt;
&lt;p&gt;Generated hey-api functions can also include &lt;code&gt;security&lt;/code&gt; metadata.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const ordersList = &amp;lt;ThrowOnError extends boolean = false&amp;gt;(
  options?: Options&amp;lt;ordersListData, ThrowOnError&amp;gt;,
) =&amp;gt;
  (options?.client ?? client).get&amp;lt;ordersListResponses, unknown, ThrowOnError&amp;gt;({
    security: [{ scheme: &quot;bearer&quot;, type: &quot;http&quot; }],
    url: &quot;/orders&quot;,
    ...options,
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a little more to understand at first, but useful for authenticated APIs.&lt;/p&gt;
&lt;h4 id=&quot;orval-2&quot;&gt;Orval&lt;/h4&gt;
&lt;p&gt;Orval is closer to raw fetch.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await ordersList(
  { page: 1 },
  {
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can pass headers per call.&lt;/p&gt;
&lt;p&gt;But in a real app, I would usually create a custom fetch with &lt;code&gt;override.mutator&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;kubb-2&quot;&gt;Kubb&lt;/h4&gt;
&lt;p&gt;Kubb can configure the client or accept per-call config.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;client.setConfig({
  baseURL: &quot;http://localhost:8000&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Headers can also be passed per call.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await ordersList(
  { page: 1 },
  {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it does not feel like axios-style interceptors are the default mental model.&lt;/p&gt;
&lt;p&gt;For things like 401 refresh, I would expect to wire my own client behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tanstack-query-zod-msw-and-faker&quot;&gt;TanStack Query, Zod, MSW, and Faker&lt;/h3&gt;
&lt;p&gt;This is where the second family of tools becomes attractive.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature         &lt;/th&gt;&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;     &lt;/th&gt;&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;                   &lt;/th&gt;&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt; &lt;/th&gt;&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Type generation &lt;/td&gt;&lt;td&gt;Good                     &lt;/td&gt;&lt;td&gt;Good                        &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;Good&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Fetch client    &lt;/td&gt;&lt;td&gt;external &lt;code&gt;openapi-fetch&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Good                        &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;Good&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TanStack Query  &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;plugin                      &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;plugin&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Zod             &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;plugin                      &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;plugin&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MSW             &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;limited / roadmap-dependent &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;Good&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Faker           &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;No                          &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;Good&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SWR / Vue Query &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;limited                     &lt;/td&gt;&lt;td&gt;Good    &lt;/td&gt;&lt;td&gt;limited / plugin-dependent&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Orval has strong built-in support around this area.&lt;/p&gt;
&lt;p&gt;It can target tools like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TanStack Query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SWR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vue Query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Zod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MSW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Faker&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kubb also has a strong plugin-based approach.&lt;/p&gt;
&lt;p&gt;hey-api is more focused around SDK generation, &lt;code&gt;TanStack Query&lt;/code&gt;, and &lt;code&gt;Zod&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If your goal is mainly API clients and types, &lt;code&gt;openapi-typescript&lt;/code&gt; or hey-api may be enough.&lt;/p&gt;
&lt;p&gt;If your goal is to generate a broader frontend ecosystem from OpenAPI, Orval or Kubb becomes more interesting.&lt;/p&gt;
&lt;p&gt;In my case, I did not need &lt;code&gt;MSW&lt;/code&gt; or &lt;code&gt;Faker&lt;/code&gt; yet.&lt;/p&gt;
&lt;p&gt;So Orval and Kubb felt a bit too much for the current problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;enum-issue-drf-spectacular-blankenum&quot;&gt;Enum issue: drf-spectacular BlankEnum&lt;/h3&gt;
&lt;p&gt;One very real-world issue was enum generation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;drf-spectacular&lt;/code&gt; can represent nullable or blank enum values like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;BlankEnum:
  enum: [&quot;&quot;]

NullEnum:
  enum: [null]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then a field may use &lt;code&gt;anyOf&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;status:
  anyOf:
    - $ref: &quot;#/components/schemas/StatusEnum&quot;
    - $ref: &quot;#/components/schemas/BlankEnum&quot;
    - $ref: &quot;#/components/schemas/NullEnum&quot;
  nullable: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The intended TypeScript value is something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;&quot;WAITING&quot; | &quot;RUNNING&quot; | &quot;SUCCESS&quot; | &quot;FAILURE&quot; | &quot;&quot; | null;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;generated-blankenum&quot;&gt;Generated BlankEnum&lt;/h4&gt;
&lt;p&gt;The generated output differed by tool.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// openapi-typescript
BlankEnum: &quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// hey-api
export enum BlankEnum {
  &quot;&quot; = &quot;&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Orval
export type BlankEnum = (typeof BlankEnum)[keyof typeof BlankEnum];

export const BlankEnum = {
  &quot;&quot;: &quot;&quot;,
} as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Kubb
export const blankEnumEnum = {} as const;
export type BlankEnumEnumKey =
  (typeof blankEnumEnum)[keyof typeof blankEnumEnum];

export type BlankEnum = BlankEnumEnumKey;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Kubb, the empty string disappeared, and the type effectively became &lt;code&gt;never&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;what-happens-after-anyof-composition&quot;&gt;What happens after anyOf composition&lt;/h4&gt;
&lt;p&gt;As a result, only Kubb dropped &lt;code&gt;&quot;&quot;&lt;/code&gt; from the final type.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                 &lt;/th&gt;&lt;th&gt;Can represent &lt;code&gt;&quot;&quot;&lt;/code&gt; in the type?&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;              &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is not just a small type detail.&lt;/p&gt;
&lt;p&gt;It can break real application code if fields like &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;file_type&lt;/code&gt;, or &lt;code&gt;update_type&lt;/code&gt; actually receive an empty string from the backend.&lt;/p&gt;
&lt;p&gt;Possible workarounds are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;patch &lt;code&gt;BlankEnum&lt;/code&gt; after generation&lt;/li&gt;
&lt;li&gt;cast affected fields to &lt;code&gt;string | null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;wait for an upstream fix&lt;/li&gt;
&lt;li&gt;avoid this schema shape if you control the backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you use &lt;code&gt;drf-spectacular&lt;/code&gt; and have many &lt;code&gt;BlankEnum&lt;/code&gt; / &lt;code&gt;NullEnum&lt;/code&gt; patterns, I recommend checking this before adopting Kubb.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;body-type-reusability&quot;&gt;Body type reusability&lt;/h3&gt;
&lt;p&gt;POST body types also differed.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                 &lt;/th&gt;&lt;th&gt;Body type style&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;components[&quot;schemas&quot;][&quot;ordersWriteRequest&quot;]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;&lt;code&gt;ordersWriteRequest&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;              &lt;/td&gt;&lt;td&gt;&lt;code&gt;ordersWriteRequest&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;&lt;code&gt;ordersCreateMutationRequest&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Kubb generated endpoint-scoped wrapper types.&lt;/p&gt;
&lt;p&gt;This can be good because each operation is very explicit.&lt;/p&gt;
&lt;p&gt;But if you want to reuse the same schema in app-level code, it can be a little less convenient.&lt;/p&gt;
&lt;p&gt;For example, if a form wants to use &lt;code&gt;ordersWriteRequest&lt;/code&gt; as its submit data type, openapi-typescript, hey-api, and Orval feel more natural.&lt;/p&gt;
&lt;p&gt;With Kubb, I may need to alias the operation-scoped type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import type { ordersCreateMutationRequest } from &quot;./api/kubb/types/ordersCreate&quot;;

type OrderWriteRequest = ordersCreateMutationRequest;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not wrong.&lt;/p&gt;
&lt;p&gt;It just depends on whether your app wants shared schema types or operation-scoped types.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lint-behavior&quot;&gt;Lint behavior&lt;/h3&gt;
&lt;p&gt;All four tools passed &lt;code&gt;tsc --noEmit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But linting showed differences.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool                 &lt;/th&gt;&lt;th&gt;oxlint default &lt;/th&gt;&lt;th&gt;oxlint + &lt;code&gt;no-explicit-any&lt;/code&gt; &lt;/th&gt;&lt;th&gt;eslint recommended&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; &lt;/td&gt;&lt;td&gt;             0 &lt;/td&gt;&lt;td&gt;                         0 &lt;/td&gt;&lt;td&gt;                36&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;             0 &lt;/td&gt;&lt;td&gt;                         0 &lt;/td&gt;&lt;td&gt;                57&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;              &lt;/td&gt;&lt;td&gt;             0 &lt;/td&gt;&lt;td&gt;                         0 &lt;/td&gt;&lt;td&gt;                36&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;             0 &lt;/td&gt;&lt;td&gt;                     1,549 &lt;/td&gt;&lt;td&gt;             1,585&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;openapi-typescript-hey-api-orval-lint-errors&quot;&gt;openapi-typescript / hey-api / Orval lint errors&lt;/h4&gt;
&lt;p&gt;For the first three tools, the eslint errors were mainly &lt;code&gt;no-irregular-whitespace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In my schema, some Django docstrings contained full-width spaces.&lt;/p&gt;
&lt;p&gt;Those comments were copied into generated code, and eslint complained.&lt;/p&gt;
&lt;p&gt;This is more about source docstrings and lint configuration than generated code quality.&lt;/p&gt;
&lt;p&gt;It can usually be handled by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;skipping comments&lt;/li&gt;
&lt;li&gt;excluding generated files from lint&lt;/li&gt;
&lt;li&gt;adding lint overrides for generated code&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;kubb-lint-errors&quot;&gt;Kubb lint errors&lt;/h4&gt;
&lt;p&gt;Kubb produced many &lt;code&gt;no-explicit-any&lt;/code&gt; errors.&lt;/p&gt;
&lt;p&gt;Most were in the runtime client side.&lt;/p&gt;
&lt;p&gt;This does not mean Kubb is unusable.&lt;/p&gt;
&lt;p&gt;But if generated files are included in lint, Kubb almost certainly needs ignore rules or overrides.&lt;/p&gt;
&lt;p&gt;Before adopting it, I would decide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;will generated files be committed?&lt;/li&gt;
&lt;li&gt;will generated files be linted?&lt;/li&gt;
&lt;li&gt;will generated files be checked in CI?&lt;/li&gt;
&lt;li&gt;which rules are disabled for generated code?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;configuration-notes&quot;&gt;Configuration notes&lt;/h3&gt;
&lt;p&gt;Here are some issues I hit while setting up each tool.&lt;/p&gt;
&lt;h4 id=&quot;openapi-typescript-2&quot;&gt;openapi-typescript&lt;/h4&gt;
&lt;p&gt;This was the easiest setup.&lt;/p&gt;
&lt;p&gt;A single CLI command worked well.&lt;/p&gt;
&lt;p&gt;I did not hit any major configuration problems.&lt;/p&gt;
&lt;h4 id=&quot;hey-api-3&quot;&gt;hey-api&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;hey-api&lt;/code&gt; uses &lt;code&gt;openapi-ts.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I hit a few small points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;output.format: &quot;prettier&quot;&lt;/code&gt; aborts if Prettier is not installed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runtimeConfigPath&lt;/code&gt; is emitted as an import path relative to &lt;code&gt;output.path&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I initially assumed &lt;code&gt;runtimeConfigPath&lt;/code&gt; was relative to the project root.&lt;/p&gt;
&lt;p&gt;That was easy to misunderstand.&lt;/p&gt;
&lt;p&gt;I shared this with the author, and the related PR was merged the next day.&lt;/p&gt;
&lt;p&gt;That fast response was a positive signal for adoption.&lt;/p&gt;
&lt;h4 id=&quot;orval-3&quot;&gt;Orval&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Orval&lt;/code&gt; uses &lt;code&gt;orval.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It can split outputs by target, which is useful if you want to generate SDKs, Zod schemas, MSW mocks, and other assets separately.&lt;/p&gt;
&lt;p&gt;I did not hit major setup issues.&lt;/p&gt;
&lt;p&gt;But if you care about auth, refresh, or error handling, you should understand &lt;code&gt;override.mutator&lt;/code&gt; early.&lt;/p&gt;
&lt;h4 id=&quot;kubb-3&quot;&gt;Kubb&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; configuration is plugin-based.&lt;/p&gt;
&lt;p&gt;Depending on your environment, generated imports with &lt;code&gt;.ts&lt;/code&gt; extensions may require this tsconfig setting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;compilerOptions&quot;: {
    &quot;allowImportingTsExtensions&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not necessarily bad, but it should match your TypeScript and bundler policy.&lt;/p&gt;
&lt;p&gt;I had to adjust the setup a few times.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-my-impression-of-openapi-typescript&quot;&gt;️ My impression of openapi-typescript&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;openapi-typescript&lt;/code&gt; is the thinnest and fastest option.&lt;/p&gt;
&lt;p&gt;It generates types and lets you choose the runtime layer yourself, often with &lt;code&gt;openapi-fetch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What I liked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;very fast generation&lt;/li&gt;
&lt;li&gt;simple setup&lt;/li&gt;
&lt;li&gt;small generated output&lt;/li&gt;
&lt;li&gt;strong focus on type correctness&lt;/li&gt;
&lt;li&gt;runtime is separate and flexible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What may be missing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no generated SDK functions&lt;/li&gt;
&lt;li&gt;endpoint calls are path-string based&lt;/li&gt;
&lt;li&gt;weaker fit for operationId-based rename refactoring&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, and &lt;code&gt;MSW&lt;/code&gt; need separate handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you value simplicity, this is very strong.&lt;/p&gt;
&lt;p&gt;Especially if codegen runs often in local development or CI, the speed is a real advantage.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-my-impression-of-hey-api&quot;&gt;️ My impression of hey-api&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;hey-api&lt;/code&gt; felt like the best balance for my use case.&lt;/p&gt;
&lt;p&gt;It is thicker than &lt;code&gt;openapi-typescript&lt;/code&gt;, but much lighter than Orval or Kubb.&lt;/p&gt;
&lt;p&gt;What I liked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generated SDK functions&lt;/li&gt;
&lt;li&gt;consistent &lt;code&gt;{ query, path, body }&lt;/code&gt; arguments&lt;/li&gt;
&lt;li&gt;axios-like interceptors&lt;/li&gt;
&lt;li&gt;result model and &lt;code&gt;throwOnError&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;shared schema types are easy to use&lt;/li&gt;
&lt;li&gt;generated output remains manageable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Weak points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generation takes longer than &lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sdk.gen.ts&lt;/code&gt; can become large&lt;/li&gt;
&lt;li&gt;weaker than Orval / Kubb for &lt;code&gt;MSW&lt;/code&gt; and &lt;code&gt;Faker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NullEnum&lt;/code&gt; may degrade into an empty enum in some cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my case, &lt;code&gt;NullEnum&lt;/code&gt; was not a major practical issue because nullable fields still carried &lt;code&gt;nullable: true&lt;/code&gt; through the composed type.&lt;/p&gt;
&lt;p&gt;More importantly, &lt;code&gt;BlankEnum&lt;/code&gt; preserved the empty string correctly.&lt;/p&gt;
&lt;p&gt;For my needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted operationId functions&lt;/li&gt;
&lt;li&gt;I wanted interceptors&lt;/li&gt;
&lt;li&gt;I wanted result-style handling&lt;/li&gt;
&lt;li&gt;I did not need generated MSW or Faker yet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So hey-api was the most practical choice.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-my-impression-of-orval&quot;&gt;️ My impression of Orval&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Orval&lt;/code&gt; is strong if you want OpenAPI to generate a broader frontend layer.&lt;/p&gt;
&lt;p&gt;What I liked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;supports &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;SWR&lt;/code&gt;, and &lt;code&gt;Vue Query&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;can generate &lt;code&gt;Zod&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;can generate &lt;code&gt;MSW&lt;/code&gt; and &lt;code&gt;Faker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;can split outputs by target&lt;/li&gt;
&lt;li&gt;faster than Kubb in this test&lt;/li&gt;
&lt;li&gt;shared body schemas are easy to reuse&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Things to watch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;default fetch client does not throw on 4xx / 5xx&lt;/li&gt;
&lt;li&gt;auth and refresh usually require a mutator&lt;/li&gt;
&lt;li&gt;file count can be large&lt;/li&gt;
&lt;li&gt;tag files can become large&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NullEnum&lt;/code&gt; may degrade into an empty const&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you only want an API client, Orval may feel too thick.&lt;/p&gt;
&lt;p&gt;But if you want generated hooks, validation schemas, mocks, and test data, Orval becomes much more attractive.&lt;/p&gt;
&lt;p&gt;If I moved deeper into the ecosystem-generation side, I would probably evaluate Orval first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-my-impression-of-kubb&quot;&gt;️ My impression of Kubb&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; has a strong philosophy.&lt;/p&gt;
&lt;p&gt;What I liked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;close to 1 operation = 1 file&lt;/li&gt;
&lt;li&gt;friendly to grep and physical file navigation&lt;/li&gt;
&lt;li&gt;structure can be tree-shake friendly&lt;/li&gt;
&lt;li&gt;clear plugin-based architecture&lt;/li&gt;
&lt;li&gt;can expand into &lt;code&gt;MSW&lt;/code&gt;, &lt;code&gt;Faker&lt;/code&gt;, &lt;code&gt;TanStack Query&lt;/code&gt;, &lt;code&gt;Zod&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;gives strong control over the codegen pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What felt heavy in this schema:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slow generation&lt;/li&gt;
&lt;li&gt;3,877 files / 24 MB output&lt;/li&gt;
&lt;li&gt;many lint errors from &lt;code&gt;any&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;possible &lt;code&gt;.ts&lt;/code&gt; import extension requirements&lt;/li&gt;
&lt;li&gt;positional path params make wrappers harder&lt;/li&gt;
&lt;li&gt;body types are endpoint-scoped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BlankEnum&lt;/code&gt; became &lt;code&gt;never&lt;/code&gt; and lost the empty string&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; is not a bad tool.&lt;/p&gt;
&lt;p&gt;Actually, I like its design philosophy.&lt;/p&gt;
&lt;p&gt;But if your main goal is simply &quot;I want a type-safe API client for a normal frontend app,&quot; it may be too heavy as the first option.&lt;/p&gt;
&lt;p&gt;I would choose Kubb when I have a clear reason, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I strongly want 1 operation = 1 file&lt;/li&gt;
&lt;li&gt;I want to design my own codegen pipeline&lt;/li&gt;
&lt;li&gt;I want plugin-level control&lt;/li&gt;
&lt;li&gt;I want to unify clients, schemas, mocks, and test data through plugins&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The potential is high, but it needs commitment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary-table&quot;&gt;Summary table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Axis                      &lt;/th&gt;&lt;th&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;     &lt;/th&gt;&lt;th&gt;&lt;code&gt;hey-api&lt;/code&gt;            &lt;/th&gt;&lt;th&gt;&lt;code&gt;Orval&lt;/code&gt;                      &lt;/th&gt;&lt;th&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Family                    &lt;/td&gt;&lt;td&gt;A: type-focused          &lt;/td&gt;&lt;td&gt;A: lightweight SDK   &lt;/td&gt;&lt;td&gt;B: frontend asset generation &lt;/td&gt;&lt;td&gt;B: plugin/codegen platform&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Generation time           &lt;/td&gt;&lt;td&gt;1.5s                     &lt;/td&gt;&lt;td&gt;8.0s                 &lt;/td&gt;&lt;td&gt;5.5s                         &lt;/td&gt;&lt;td&gt;18.1s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Output files              &lt;/td&gt;&lt;td&gt;1                        &lt;/td&gt;&lt;td&gt;16                   &lt;/td&gt;&lt;td&gt;2,719                        &lt;/td&gt;&lt;td&gt;3,877&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Output size               &lt;/td&gt;&lt;td&gt;2.4 MB                   &lt;/td&gt;&lt;td&gt;2.9 MB               &lt;/td&gt;&lt;td&gt;14 MB                        &lt;/td&gt;&lt;td&gt;24 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Generates functions       &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;Yes                  &lt;/td&gt;&lt;td&gt;Yes                          &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Function split            &lt;/td&gt;&lt;td&gt;none                     &lt;/td&gt;&lt;td&gt;one SDK file         &lt;/td&gt;&lt;td&gt;one file per tag             &lt;/td&gt;&lt;td&gt;one file per operation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error model               &lt;/td&gt;&lt;td&gt;result union             &lt;/td&gt;&lt;td&gt;result union / throw &lt;/td&gt;&lt;td&gt;success wrapper              &lt;/td&gt;&lt;td&gt;throw&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Client object             &lt;/td&gt;&lt;td&gt;external library         &lt;/td&gt;&lt;td&gt;generated client     &lt;/td&gt;&lt;td&gt;raw fetch / mutator          &lt;/td&gt;&lt;td&gt;generated client&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Interceptor               &lt;/td&gt;&lt;td&gt;openapi-fetch middleware &lt;/td&gt;&lt;td&gt;axios-like           &lt;/td&gt;&lt;td&gt;mutator                      &lt;/td&gt;&lt;td&gt;config / custom client&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Path params               &lt;/td&gt;&lt;td&gt;&lt;code&gt;params.path&lt;/code&gt;            &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ path: { id } }&lt;/code&gt;   &lt;/td&gt;&lt;td&gt;&lt;code&gt;{ id }&lt;/code&gt;                     &lt;/td&gt;&lt;td&gt;positional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Body type                 &lt;/td&gt;&lt;td&gt;shared schema            &lt;/td&gt;&lt;td&gt;shared schema        &lt;/td&gt;&lt;td&gt;shared schema                &lt;/td&gt;&lt;td&gt;endpoint-scoped&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;TanStack Query            &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;plugin               &lt;/td&gt;&lt;td&gt;built-in support             &lt;/td&gt;&lt;td&gt;plugin&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Zod                       &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;plugin               &lt;/td&gt;&lt;td&gt;built-in support             &lt;/td&gt;&lt;td&gt;plugin&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MSW / Faker               &lt;/td&gt;&lt;td&gt;No                       &lt;/td&gt;&lt;td&gt;limited              &lt;/td&gt;&lt;td&gt;strong                       &lt;/td&gt;&lt;td&gt;strong&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Lint                      &lt;/td&gt;&lt;td&gt;minor adjustment         &lt;/td&gt;&lt;td&gt;minor adjustment     &lt;/td&gt;&lt;td&gt;minor adjustment             &lt;/td&gt;&lt;td&gt;likely needs ignore/override&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;drf-spectacular BlankEnum &lt;/td&gt;&lt;td&gt;OK                       &lt;/td&gt;&lt;td&gt;OK                   &lt;/td&gt;&lt;td&gt;OK                           &lt;/td&gt;&lt;td&gt;needs care&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Best fit                  &lt;/td&gt;&lt;td&gt;thin and safe            &lt;/td&gt;&lt;td&gt;SDK + interceptors   &lt;/td&gt;&lt;td&gt;generate frontend assets     &lt;/td&gt;&lt;td&gt;design codegen pipeline&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-i-would-choose&quot;&gt;How I would choose&lt;/h3&gt;
&lt;p&gt;After this comparison, I would choose like this.&lt;/p&gt;
&lt;h4 id=&quot;1-start-with-openapi-typescript&quot;&gt;1. Start with openapi-typescript&lt;/h4&gt;
&lt;p&gt;If your project already works well with &lt;code&gt;openapi-typescript + openapi-fetch&lt;/code&gt;, I would stay there.&lt;/p&gt;
&lt;p&gt;It is fast, thin, and easy to understand.&lt;/p&gt;
&lt;p&gt;A small wrapper like &lt;code&gt;src/api/client.ts&lt;/code&gt; plus middleware-based auth may be enough.&lt;/p&gt;
&lt;p&gt;There is no need to move to heavier codegen unless you have a concrete pain.&lt;/p&gt;
&lt;h4 id=&quot;2-move-to-hey-api-when-sdk-functions-become-useful&quot;&gt;2. Move to hey-api when SDK functions become useful&lt;/h4&gt;
&lt;p&gt;The next step is usually wanting things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;call APIs by operationId functions instead of URL strings&lt;/li&gt;
&lt;li&gt;grep by function name&lt;/li&gt;
&lt;li&gt;use rename refactoring&lt;/li&gt;
&lt;li&gt;handle auth and refresh with interceptors&lt;/li&gt;
&lt;li&gt;work with consistent endpoint options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that stage, &lt;code&gt;hey-api&lt;/code&gt; is a very good fit.&lt;/p&gt;
&lt;h4 id=&quot;3-use-orval-when-openapi-becomes-the-source-of-frontend-assets&quot;&gt;3. Use Orval when OpenAPI becomes the source of frontend assets&lt;/h4&gt;
&lt;p&gt;If OpenAPI becomes more than API client generation, &lt;code&gt;Orval&lt;/code&gt; becomes strong.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generated &lt;code&gt;TanStack Query&lt;/code&gt; hooks&lt;/li&gt;
&lt;li&gt;generated &lt;code&gt;Zod&lt;/code&gt; schemas&lt;/li&gt;
&lt;li&gt;generated &lt;code&gt;MSW&lt;/code&gt; mocks&lt;/li&gt;
&lt;li&gt;generated fake data&lt;/li&gt;
&lt;li&gt;multiple generation targets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where Orval's value becomes much clearer.&lt;/p&gt;
&lt;h4 id=&quot;4-choose-kubb-when-you-love-the-architecture&quot;&gt;4. Choose Kubb when you love the architecture&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Kubb&lt;/code&gt; is powerful.&lt;/p&gt;
&lt;p&gt;But at this schema size, it was also heavy.&lt;/p&gt;
&lt;p&gt;I would choose it when I strongly care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 operation = 1 file&lt;/li&gt;
&lt;li&gt;plugin architecture&lt;/li&gt;
&lt;li&gt;tree-shake-friendly structure&lt;/li&gt;
&lt;li&gt;codegen pipeline control&lt;/li&gt;
&lt;li&gt;generating many kinds of artifacts consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It may not be my first choice for a normal frontend app, but it is very interesting if you want to design codegen itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-conclusion&quot;&gt;Final conclusion&lt;/h3&gt;
&lt;p&gt;OpenAPI codegen should not be chosen only by asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which tool has the most features?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A better question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How much generated code are we willing to own?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My final choice looks like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Goal                                            &lt;/th&gt;&lt;th&gt;Choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;I only want types                               &lt;/td&gt;&lt;td&gt;&lt;code&gt;openapi-typescript&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;I want SDK functions and interceptors           &lt;/td&gt;&lt;td&gt;&lt;code&gt;hey-api&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;I want to generate frontend assets together     &lt;/td&gt;&lt;td&gt;&lt;code&gt;Orval&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;I want to control codegen structure and plugins &lt;/td&gt;&lt;td&gt;&lt;code&gt;Kubb&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For my current project, &lt;code&gt;hey-api&lt;/code&gt; had the best balance of SDK functions, interceptors, result handling, and output size.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i70tk9kuqwxr09haz4hg.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;The more useful codegen becomes, the bigger the generated forest becomes.&lt;/p&gt;
&lt;p&gt;So the important question is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who maintains that forest, how much of it, and with what team rules?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is the real OpenAPI codegen decision.&lt;/p&gt;
&lt;p&gt;What does your team generate from OpenAPI?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only types?&lt;/li&gt;
&lt;li&gt;SDK functions?&lt;/li&gt;
&lt;li&gt;Hooks?&lt;/li&gt;
&lt;li&gt;Mocks?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;or the whole forest? &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24486/which-openapi-codegen-should-you-choose-openapi-typescript-vs-hey-api-vs-orval-vs-kubb</guid>
<pubDate>Wed, 12 Aug 2026 09:22:00 +0000</pubDate>
</item>
<item>
<title>Running OpenAPI Validation in GitHub Actions and Showing Findings in Pull Requests</title>
<link>https://coderlegion.com/21636/running-openapi-validation-in-github-actions-and-showing-findings-in-pull-requests</link>
<description>
&lt;p&gt;Hello, I'm Ganesh. I'm building &lt;em&gt;git-lrc&lt;/em&gt;, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. &lt;a href=&quot;https://github.com/HexmosTech/git-lrc?utm_source=chatgpt.com&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Star git-lrc on GitHub&lt;/a&gt; to help more developers discover the project. Do give it a try and share your feedback for improving the product.&lt;/p&gt;

&lt;p&gt;In a previous article, I explained what SARIF is and why many security and quality tools use it as a common reporting format.&lt;/p&gt;

&lt;p&gt;In this article, we'll focus on a practical example: validating an OpenAPI specification in GitHub Actions and displaying findings directly inside GitHub Pull Requests.&lt;/p&gt;

&lt;p&gt;By the end, you'll have a workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lints your OpenAPI specification&lt;/li&gt;
&lt;li&gt;Generates a SARIF report&lt;/li&gt;
&lt;li&gt;Uploads results to GitHub Code Scanning&lt;/li&gt;
&lt;li&gt;Shows annotations directly in Pull Requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sample OpenAPI Specification
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple OpenAPI file that contains a deliberate issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;openapi&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;3.0.3&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;User API&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;1.0.0&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;/users/{id}&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;operationId&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;getUserById&lt;/span&gt;

      &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;userId&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;in&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;path&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;string&lt;/span&gt;

      &lt;span class=&quot;na&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;200&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;User found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight plaintext&quot;&gt;&lt;code&gt;/users/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the parameter is named:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight plaintext&quot;&gt;&lt;code&gt;userId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameter name should match the path placeholder (&lt;code&gt;id&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We'll use this mistake to verify that our workflow correctly reports findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Spectral
&lt;/h2&gt;

&lt;p&gt;For this example, we'll use Spectral, one of the most popular OpenAPI linting tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; @stoplight/spectral-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;spectral lint openapi.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an error related to the path parameter mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating a SARIF Report
&lt;/h2&gt;

&lt;p&gt;Instead of printing results to the console, we can generate a SARIF report:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight shell&quot;&gt;&lt;code&gt;spectral lint openapi.yaml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; sarif &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt; results.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight plaintext&quot;&gt;&lt;code&gt;results.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which GitHub can consume directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight plaintext&quot;&gt;&lt;code&gt;.github/workflows/openapi.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;OpenAPI Validation&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;pull_request&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;read&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;security-events&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;write&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;openapi&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;20&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Install Spectral&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;npm install -g @stoplight/spectral-cli&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Generate SARIF Report&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;spectral lint openapi.yaml \&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;--format sarif \&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;--output results.sarif&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Upload SARIF&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;sarif_file&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;results.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The workflow performs four simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks out the repository&lt;/li&gt;
&lt;li&gt;Installs Spectral&lt;/li&gt;
&lt;li&gt;Generates a SARIF report&lt;/li&gt;
&lt;li&gt;Uploads the SARIF report to GitHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The upload step is handled by GitHub's official SARIF uploader:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;sarif_file&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;results.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once uploaded, GitHub automatically processes the findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing Results in Pull Requests
&lt;/h2&gt;

&lt;p&gt;After opening a Pull Request, GitHub analyzes the uploaded SARIF report and associates findings with the corresponding files and lines.&lt;/p&gt;

&lt;p&gt;For our example, GitHub highlights the parameter definition and reports something similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class=&quot;highlight js-code-highlight&quot;&gt;
&lt;pre class=&quot;highlight plaintext&quot;&gt;&lt;code&gt;Path parameter &quot;id&quot; is not defined.
Expected parameter name &quot;id&quot; but found &quot;userId&quot;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers can review the issue directly from the Pull Request without searching through GitHub Action logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Prefer This Approach
&lt;/h2&gt;

&lt;p&gt;Many teams fail OpenAPI validation jobs and require developers to inspect CI logs.&lt;/p&gt;

&lt;p&gt;While this works, it doesn't scale well when repositories contain multiple specifications or many validation rules.&lt;/p&gt;

&lt;p&gt;Uploading SARIF results provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inline annotations&lt;/li&gt;
&lt;li&gt;Better visibility during code review&lt;/li&gt;
&lt;li&gt;Centralized findings in GitHub Code Scanning&lt;/li&gt;
&lt;li&gt;Consistent reporting across different tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same workflow can later be extended to include security scanners, secret scanners, IaC scanners, and custom validation tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating OpenAPI validation into GitHub Actions is straightforward. With Spectral generating SARIF output and GitHub handling the presentation layer, developers receive feedback exactly where they are already reviewing code: inside the Pull Request.&lt;/p&gt;

&lt;p&gt;If your organization already uses SARIF for other security or quality tools, OpenAPI validation can fit naturally into the same workflow with only a few lines of configuration.&lt;br&gt;
&lt;a href=&quot;https://github.com/HexmosTech/git-lrc&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;&lt;img src=&quot;https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuphqwcp6bha9ol6pdo3.png&quot; alt=&quot;git-lrc&quot; width=&quot;360&quot; height=&quot;540&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any feedback or contributors are welcome! It&amp;rsquo;s online, source-available, and ready for anyone to use.&lt;/p&gt;

&lt;p&gt;&amp;#11088; &lt;a href=&quot;https://github.com/HexmosTech/git-lrc?utm_source=chatgpt.com&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;Star git-lrc on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21636/running-openapi-validation-in-github-actions-and-showing-findings-in-pull-requests</guid>
<pubDate>Sat, 04 Jul 2026 00:49:15 +0000</pubDate>
</item>
</channel>
</rss>