<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: D.S.</title>
    <description>The latest articles on Forem by D.S. (@gunzip_).</description>
    <link>https://forem.com/gunzip_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3490643%2Fe0f11b1e-0cd0-4fe2-8ed0-cba0dd3b3958.png</url>
      <title>Forem: D.S.</title>
      <link>https://forem.com/gunzip_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gunzip_"/>
    <language>en</language>
    <item>
      <title>How OpenAPI Undermines a Good Developer Experience</title>
      <dc:creator>D.S.</dc:creator>
      <pubDate>Fri, 03 Oct 2025 09:36:41 +0000</pubDate>
      <link>https://forem.com/gunzip_/how-openapi-undermines-a-good-developer-experience-344m</link>
      <guid>https://forem.com/gunzip_/how-openapi-undermines-a-good-developer-experience-344m</guid>
      <description>&lt;p&gt;In the JavaScript/TypeScript ecosystem, tools like &lt;a href="https://trpc.io/" rel="noopener noreferrer"&gt;tRPC&lt;/a&gt; and &lt;a href="https://orpc.unnoq.com/" rel="noopener noreferrer"&gt;oRPC&lt;/a&gt; are gaining traction. This isn’t by chance, it signals a growing need in the JS world: moving beyond the &lt;strong&gt;Developer Experience&lt;/strong&gt; imposed by OpenAPI’s schema-first approach. But why?&lt;/p&gt;

&lt;h2&gt;
  
  
  All OpenAPI Code Generators Are Broken
&lt;/h2&gt;

&lt;p&gt;With the help of Gen-AI, I'm building a &lt;a href="https://gunzip.github.io/apical-ts/" rel="noopener noreferrer"&gt;tool to generate TypeScript code from OpenAPI specs&lt;/a&gt;. Fighting against the standard, I uncovered a systemic issue: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All existing code generators are unreliable, and it couldn’t be otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;OpenAPI specs are unnecessarily complex&lt;/strong&gt;, creating a chaotic mess for anyone trying to automate code generation. Here’s why.&lt;/p&gt;

&lt;h3&gt;
  
  
   Error Handling Chaos
&lt;/h3&gt;

&lt;p&gt;Error handling in OpenAPI requires a sisyphean effort. You must manually map different layers: &lt;strong&gt;network errors&lt;/strong&gt; (failed &lt;code&gt;fetch&lt;/code&gt;), &lt;strong&gt;server errors&lt;/strong&gt; (&lt;code&gt;5xx&lt;/code&gt;), &lt;strong&gt;client errors&lt;/strong&gt; (&lt;code&gt;4xx&lt;/code&gt;), and payload serialization/deserialization issues. &lt;/p&gt;

&lt;p&gt;Each status code - (ie. &lt;code&gt;404&lt;/code&gt; returning nothing, &lt;code&gt;429&lt;/code&gt; providing retry parameters, &lt;code&gt;400&lt;/code&gt; delivering unpredictable serialization errors, ...) may (or not) come with a payload in wildly different formats with disconnected meanings.&lt;/p&gt;

&lt;p&gt;Therefore, a client generator cannot merely "implement retries": &lt;strong&gt;users must handle all this logic themselves&lt;/strong&gt; by interpreting the returned data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serialization Nightmare
&lt;/h3&gt;

&lt;p&gt;Take &lt;a href="https://swagger.io/docs/specification/v3_0/serialization/" rel="noopener noreferrer"&gt;parameter serialization&lt;/a&gt; (ie. &lt;code&gt;style&lt;/code&gt; and &lt;code&gt;explode&lt;/code&gt;). You can send textual data to an endpoint in seven different ways with the same outcome. This redundancy is there for legacy reasons, pointless and breeds ambiguity everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Schema Quirks
&lt;/h3&gt;

&lt;p&gt;JSON Schema is a minefield of edge cases. For example, you can have an allOf intersection with and empty object that specifies a list of required fields without any schema attached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;SomeSchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allOf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/SomeOtherSchema"&lt;/span&gt;        
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/SomeOtherSchemaAgain"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;foo&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator implementation must traverse all fields of all objects in the intersection, only to mark as required - if any - those referenced by this empty object. Guess what? No know tool handles this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wildcard Complexity
&lt;/h3&gt;

&lt;p&gt;Generators must handle vague cases like range status codes (e.g., &lt;code&gt;4XX&lt;/code&gt;, &lt;code&gt;5XX&lt;/code&gt;, ...) alongside the "&lt;code&gt;default&lt;/code&gt;" case, adding yet another layer of complexity that’s nearly impossible to manage keeping a good DX.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Challenge
&lt;/h2&gt;

&lt;p&gt;With so many intersections and details, no tool can handle every scenario. TypeScript developers today are working with tools that are merely "good enough," if that’s even sufficient.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What’s the point of a spec if its core purpose - automatic code generation - is inherently flawed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All existing TypeScript generators are approximate at best. You can verify this yourself against a &lt;a href="https://gist.githubusercontent.com/gunzip/6f42c323ba2b7b099a8fd0a0fcfef9ca/raw/beeeb2fdec7a1b3ef5b825ee9f9cbbbd589f93b7/merge.yaml" rel="noopener noreferrer"&gt;valid OpenAPI spec&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Try find a generator that correctly handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different schemas for multiple &lt;code&gt;2xx&lt;/code&gt; responses&lt;/li&gt;
&lt;li&gt;Different schemas per content type&lt;/li&gt;
&lt;li&gt;Default responses&lt;/li&gt;
&lt;li&gt;Range status codes&lt;/li&gt;
&lt;li&gt;Intersections with empty objects&lt;/li&gt;
&lt;li&gt;Self references&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Time to Rethink the Paradigm
&lt;/h2&gt;

&lt;p&gt;Recently, I came across the &lt;a href="https://blog.cloudflare.com/capnweb-javascript-rpc-library/" rel="noopener noreferrer"&gt;Cap'n Web protocol&lt;/a&gt; from Cloudflare, and it clearly demonstrates a unified approach that delivers a DX unthinkable with OpenAPI.&lt;/p&gt;

&lt;p&gt;OpenAPI solves problems that shouldn’t exist in the first place. The schema-first approach is doomed to deliver a subpar DX. It's so poor that Microsoft decided to implement &lt;a href="https://typespec.io/" rel="noopener noreferrer"&gt;its own alternative DSL&lt;/a&gt; just to produce OpenAPI specs. Entire commercial companies have their core product built around being a decent OpenAPI code generator (!).&lt;/p&gt;

&lt;p&gt;For a new project - especially internal services - I would rather lean towards DX-friendly alternatives like modern RPC protocols or a code-first approach where the API specification is generated directly from the source code.&lt;/p&gt;

&lt;p&gt;Are you using OpenAPI for internal APIs, or have you switched to something more streamlined?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>typescript</category>
      <category>discuss</category>
      <category>api</category>
    </item>
    <item>
      <title>OpenAPI to TypeScript: a More Reliable and Type-Safe Approach</title>
      <dc:creator>D.S.</dc:creator>
      <pubDate>Sun, 14 Sep 2025 22:42:32 +0000</pubDate>
      <link>https://forem.com/gunzip_/typescript-devs-dont-let-your-openapi-client-generator-lie-to-you-47d6</link>
      <guid>https://forem.com/gunzip_/typescript-devs-dont-let-your-openapi-client-generator-lie-to-you-47d6</guid>
      <description>&lt;p&gt;If you’re building REST API clients (or servers) with TypeScript, you expect type safety to save the day. But most existing OpenAPI-to-Typescript generators give a false sense of security, hiding pitfalls that can bite in production. After benchmarking 20+ tools, here’s what I found, and how I choose to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poor error handling
&lt;/h2&gt;

&lt;p&gt;Calling&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should be safe, but the method signature often ignores network issues like DNS failures or timeouts. You’re forced to wrap it in a &lt;code&gt;try/catch&lt;/code&gt;, but without clear documentation on what errors to expect, you’re left guessing. Even when documented, this approach is fragile and prevents efficient error handling as validation errors (versus OpenAPI specs) end up in the same bag as network errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limited handling of HTTP statuses
&lt;/h2&gt;

&lt;p&gt;Most generators emit types only for 2xx responses, ignoring 4xx/5xx errors with specific payloads. If the API returns a 400 or 500 with some &lt;code&gt;ProblemJson&lt;/code&gt;, generated TypeScript types (and/or runtime schemas) won’t reflect it, leaving you vulnerable to runtime surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple success responses, different payloads
&lt;/h2&gt;

&lt;p&gt;Some APIs return different payloads for 200 vs. 201 status codes. Many generated clients only handle the &lt;em&gt;first&lt;/em&gt; successful status code. Other clients either treat non-200 responses as unknown errors, or merge them into some vague Typescript union, forcing further runtime checks to discriminate. What’s the point of using the generated code then?&lt;/p&gt;

&lt;h2&gt;
  
  
  Mishandled default responses
&lt;/h2&gt;

&lt;p&gt;The smartest generators create &lt;strong&gt;discriminated unions&lt;/strong&gt; for all response types (200, 201, 400, 500, etc.), but &lt;code&gt;default&lt;/code&gt; responses (covering unspecified cases in OpenAPI) are treated as generic errors and/or lack payload typing and validation, leading to inaccurate types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content-type being ignored
&lt;/h2&gt;

&lt;p&gt;APIs returning multiple content types require discrimination by both status code and content-type. Afaik, no existing Typescript generator supports this. Ideally, you should be able to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Typescript should know that r.data is SomeJsonModel&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/xml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Typescript should know that r.data is SomeXmlModel&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  OpenAPI Schema mismatch
&lt;/h2&gt;

&lt;p&gt;OpenAPI schemas are more expressive than TypeScript types. For example, a schema defining an email string or regex pattern often becomes a plain string in TypeScript. Even tools with runtime validation struggle to support advanced OpenAPI features like string patterns or &lt;code&gt;oneOf&lt;/code&gt; vs &lt;code&gt;anyOf&lt;/code&gt;, leading to inaccurate types and runtime bugs.&lt;/p&gt;

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

&lt;p&gt;After evaluating the landscape, I faced a dilemma: convince maintainers of top tools to adopt breaking changes for their thousands of users or build a new solution from scratch. I chose the latter and built an open-source TypeScript client generator that prioritizes safe, &lt;strong&gt;accurate types&lt;/strong&gt; and excellent Developer Experience:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gunzip.github.io/apical-ts/" rel="noopener noreferrer"&gt;https://gunzip.github.io/apical-ts/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's your biggest pain point with generated OpenAPI clients / servers?&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts to shape this project, drop a comment, or check out the prototype!&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>openapi</category>
      <category>zod</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
