<?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>DEV Community: Lucas Dachman</title>
    <description>The latest articles on DEV Community by Lucas Dachman (@lucasdachman).</description>
    <link>https://dev.to/lucasdachman</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%2F1040726%2Fe071113e-50ff-4335-ac5e-8d40d5e48930.png</url>
      <title>DEV Community: Lucas Dachman</title>
      <link>https://dev.to/lucasdachman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucasdachman"/>
    <language>en</language>
    <item>
      <title>How I Generate Both My Kotlin Backend and TypeScript Frontend from One Spec</title>
      <dc:creator>Lucas Dachman</dc:creator>
      <pubDate>Tue, 06 Jan 2026 16:33:01 +0000</pubDate>
      <link>https://dev.to/lucasdachman/how-i-generate-both-my-kotlin-backend-and-typescript-frontend-from-one-spec-5cd6</link>
      <guid>https://dev.to/lucasdachman/how-i-generate-both-my-kotlin-backend-and-typescript-frontend-from-one-spec-5cd6</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you're building a full-stack app with JavaScript on both ends, you've got options. tRPC, Next.js server actions, Remix loaders—these let you share types directly between frontend and backend. Nice.&lt;/p&gt;

&lt;p&gt;But if your backend is Kotlin, Go, Rust, or anything that isn't JavaScript? You're back to the old problem. The backend team nests &lt;code&gt;user.email&lt;/code&gt; inside a new &lt;code&gt;user.profile&lt;/code&gt; object. The frontend still reads &lt;code&gt;user.email&lt;/code&gt;. Nobody catches it until production.&lt;/p&gt;

&lt;p&gt;Or this one: the backend changes a field from required to optional, the frontend still assumes it's always there, and now you've got &lt;code&gt;undefined is not an object&lt;/code&gt; in production.&lt;/p&gt;

&lt;p&gt;I've been building web apps for a while. These problems keep showing up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract drift.&lt;/strong&gt; The backend and frontend have their own ideas about what the API looks like. They slowly diverge until something breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boilerplate everywhere.&lt;/strong&gt; Every endpoint means writing route definitions on the server, then matching types in TypeScript, then fetch calls. If you're using OpenAPI for docs, you're writing the same information again in annotations. Same thing, four or five different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime surprises.&lt;/strong&gt; TypeScript's type safety is great—until your types don't match reality. Then it's worse than no types at all, because you trusted them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation rot.&lt;/strong&gt; You write API docs, they're accurate for about a week, then they slowly become fiction.&lt;/p&gt;

&lt;p&gt;It's all the same problem: no single source of truth. The "real" API ends up existing in the backend handlers, the frontend types, someone's head—and they're constantly out of sync.&lt;/p&gt;

&lt;p&gt;When I started building &lt;a href="https://subflag.com" rel="noopener noreferrer"&gt;Subflag&lt;/a&gt;, a feature flag service, I ran into this immediately. Kotlin/Ktor backend, React/TypeScript frontend. Organizations, projects, flags, targeting rules, environments—70 endpoints across 16 API domains. Plenty of surface area for drift.&lt;/p&gt;

&lt;p&gt;I wanted one source that defined the API. Everything else gets generated from it.&lt;/p&gt;

&lt;p&gt;Here's how that went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The OpenAPI-First Approach
&lt;/h2&gt;

&lt;p&gt;Two ways to use OpenAPI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code-first&lt;/strong&gt;: Write your backend, add annotations, generate the spec from your code. The code is the source of truth. The spec is a byproduct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-first&lt;/strong&gt;: Write the OpenAPI spec first, generate code from it. The spec is the source of truth. The code has to match.&lt;/p&gt;

&lt;p&gt;I went with spec-first. Here's why.&lt;/p&gt;

&lt;p&gt;Code-first doesn't actually fix the drift problem. Sure, the spec describes what your backend does right now. But your frontend types are still written by hand. Change the backend, regenerate the spec, and... the frontend still has stale types. Unless someone remembers to update them.&lt;/p&gt;

&lt;p&gt;And the spec itself? It's always playing catch-up. When you're rushing to ship, nobody's updating annotations. The documentation drifts too.&lt;/p&gt;

&lt;p&gt;Spec-first flips this around. You edit one YAML file, run the generators, and both your Kotlin server interfaces and TypeScript client types update. Change the spec, everything follows.&lt;/p&gt;

&lt;p&gt;The spec becomes the contract both sides compile against. Frontend expects a field the spec doesn't define? TypeScript catches it. Backend handler doesn't match the interface? Kotlin catches it. Drift becomes a compile error, not a production bug.&lt;/p&gt;

&lt;p&gt;It also means documentation can't be an afterthought. With code-first, the spec comes &lt;em&gt;after&lt;/em&gt; you write the code—so good descriptions and examples are extra work you do later. Or never. With spec-first, you're writing docs as you design the API.&lt;/p&gt;

&lt;p&gt;The tradeoff: you have to write YAML before you write code. Some teams hate that. For me, it's become the natural way to think about an API—figure out the contract, then implement it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                         api.yml                                 │
│                    (Single Source of Truth)                     │
└─────────────────────┬───────────────────────┬───────────────────┘
                      │                       │
                      ▼                       ▼
        ┌─────────────────────┐   ┌─────────────────────┐
        │   Kotlin Generator  │   │ TypeScript Generator│
        │   (./gradlew build) │   │  (pnpm run generate)│
        └──────────┬──────────┘   └──────────┬──────────┘
                   │                         │
                   ▼                         ▼
        ┌─────────────────────┐   ┌─────────────────────┐
        │ Handler interfaces  │   │ API client classes  │
        │ Request/Response    │   │ TypeScript types    │
        │ DTOs                │   │                     │
        └──────────┬──────────┘   └──────────┬──────────┘
                   │                         │
                   ▼                         ▼
        ┌─────────────────────┐   ┌─────────────────────┐
        │ My handler impls    │   │ React components    │
        │ (business logic)    │   │ (uses typed client) │
        └─────────────────────┘   └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;api.yml&lt;/code&gt; feeds two generators. Kotlin side gets server interfaces and DTOs. TypeScript side gets a typed API client. I just write business logic against the generated interfaces.&lt;/p&gt;

&lt;p&gt;I'm using &lt;a href="https://openapi-generator.tech/" rel="noopener noreferrer"&gt;OpenAPI Generator&lt;/a&gt;—an open source project with generators for 50+ languages and frameworks. It can produce server stubs, client SDKs, or both. You pick the generator that matches your stack.&lt;/p&gt;

&lt;p&gt;For scale: my spec is about 3,300 lines of YAML. 70 endpoints across 16 API domains (auth, flags, environments, targeting, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Kotlin Side
&lt;/h3&gt;

&lt;p&gt;I use the &lt;a href="https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-gradle-plugin" rel="noopener noreferrer"&gt;OpenAPI Generator Gradle plugin&lt;/a&gt; with the &lt;code&gt;kotlin-server&lt;/code&gt; generator targeting Ktor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;named&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GenerateTask&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"openApiGenerate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;generatorName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kotlin-server"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;inputSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${rootDir}/api.yml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"generated"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;asFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;templateDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$rootDir/template"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// Custom templates&lt;/span&gt;
    &lt;span class="n"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.subflag.generated"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;modelNameSuffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dto"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// Generated types get Dto suffix&lt;/span&gt;
    &lt;span class="n"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"library"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"ktor2"&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;The &lt;code&gt;modelNameSuffix&lt;/code&gt; keeps generated DTOs separate from my domain models. &lt;code&gt;CreateFlagRequestDto&lt;/code&gt; comes from the generator; &lt;code&gt;Flag&lt;/code&gt; is my domain type.&lt;/p&gt;

&lt;h3&gt;
  
  
  The TypeScript Side
&lt;/h3&gt;

&lt;p&gt;Frontend generation is simpler—just a shell script that runs before dev/build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openapi-generator-cli generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; ../server/api.yml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; typescript-axios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ./generated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--additional-properties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;useSingleRequestParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me typed API classes wrapping Axios. Full autocomplete, full type checking on every API call.&lt;/p&gt;

&lt;h4&gt;
  
  
  Wiring Up Authentication
&lt;/h4&gt;

&lt;p&gt;The generated classes take a custom Axios instance. So I set up one instance with auth interceptors:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiAxios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;withCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// for refresh token cookie&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Attach JWT to every request&lt;/span&gt;
&lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interceptors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&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;token&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Handle auth errors globally&lt;/span&gt;
&lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interceptors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// session expired, redirect to login&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;Then all generated API classes use this instance:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagsApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuthApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectsApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProjectsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... 14 more&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth lives in one place. The generated code just uses whatever Axios instance you hand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Developer Workflow
&lt;/h2&gt;

&lt;p&gt;Here's what adding an endpoint actually looks like. I'll use "create flag" as an example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Endpoint in the Spec
&lt;/h3&gt;

&lt;p&gt;Add the endpoint to &lt;code&gt;api.yml&lt;/code&gt;:&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;/api/flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Flags&lt;/span&gt;
    &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;auth-jwt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
    &lt;span class="na"&gt;parameters&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/parameters/OrganizationParamReq'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/parameters/ProjectParamReq'&lt;/span&gt;
    &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;schema&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/CreateFlagRequest'&lt;/span&gt;
    &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Created - Flag created successfully&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;schema&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Flag'&lt;/span&gt;
    &lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;409'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Conflict - Flag with this name already exists&lt;/span&gt;
    &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;createFlag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;operationId&lt;/code&gt; becomes the function name in generated code. The &lt;code&gt;tags&lt;/code&gt; field determines the interface name. For example, all endpoints tagged with &lt;code&gt;Flags&lt;/code&gt; get defined in &lt;code&gt;FlagsApiHandler&lt;/code&gt;, &lt;code&gt;Auth&lt;/code&gt; endpoints in &lt;code&gt;AuthApiHandler&lt;/code&gt;, etc. That's how 70 endpoints stay organized into 16 interfaces.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;security: auth-jwt&lt;/code&gt; means the generated route gets wrapped in authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Run the Kotlin Generator
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;./gradlew build&lt;/code&gt; regenerates everything. The generator produces an interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FlagsApiHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;createFlag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;deleteFlag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;listFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
    &lt;span class="c1"&gt;// ... one property per operationId&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a routing function that wires it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FlagsApiHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth-jwt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/flags"&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createFlag&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;span class="c1"&gt;// ... rest of the routes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I write &lt;code&gt;/api/flags&lt;/code&gt; once in the spec. The generator handles the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Implement the Handler
&lt;/h3&gt;

&lt;p&gt;I create an object that implements the generated interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;FlagsHandlers&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FlagsApiHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// `createFlag` is the operationId from the spec&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;createFlag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Auth checks, get project context...&lt;/span&gt;

        &lt;span class="c1"&gt;// Receive request using GENERATED DTO type&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateFlagRequestDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// Validation and business logic...&lt;/span&gt;

        &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;FlagDto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&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;&lt;code&gt;CreateFlagRequestDto&lt;/code&gt; and &lt;code&gt;FlagDto&lt;/code&gt; come from the generator. Mistype a field? Compiler catches it. Spec changes? DTOs change, and my code won't compile until I fix it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Wire It Up
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;Routing.kt&lt;/code&gt;, I add one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FlagsHandlers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// That's it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 16 of my APIs are wired up the same way—each a single line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Use the TypeScript Client
&lt;/h3&gt;

&lt;p&gt;On the frontend, &lt;code&gt;pnpm run generate-api&lt;/code&gt; regenerates the client. In React:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlagsApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@generated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flagsApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiAxios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use it:&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="c1"&gt;// The operationId `createFlag` becomes a method&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;flagsApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFlag&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;organizationName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createFlagRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;flagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;valueType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BOOLEAN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;span class="c1"&gt;// response.data is typed as Flag&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newFlag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a required field to the spec? TypeScript errors until I provide it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Feedback Loop
&lt;/h3&gt;

&lt;p&gt;The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Edit &lt;code&gt;api.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;./gradlew build&lt;/code&gt; → Kotlin interface updates&lt;/li&gt;
&lt;li&gt;Compiler tells me what to implement&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pnpm run generate-api&lt;/code&gt; → TypeScript types update&lt;/li&gt;
&lt;li&gt;TypeScript tells me what to fix in the frontend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No manual syncing. No "did I remember to update the types?" The spec drives everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom Kotlin Templates: Making It Actually Work
&lt;/h2&gt;

&lt;p&gt;Here's the thing: the default OpenAPI Generator templates are built for the common case. My case (Ktor 2 with kotlinx.serialization) wasn't common enough.&lt;/p&gt;

&lt;p&gt;The generator uses &lt;a href="https://mustache.github.io/" rel="noopener noreferrer"&gt;Mustache templates&lt;/a&gt; to produce code. You can override any template by dropping a modified version in a &lt;code&gt;template/&lt;/code&gt; directory. I started with the &lt;code&gt;ktor2&lt;/code&gt; templates and modified them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Handler Interface Pattern
&lt;/h3&gt;

&lt;p&gt;This is the big one. Default templates generate inline route handlers—your business logic lives inside the generated code. Problem: you can't edit generated code. It gets overwritten on the next regeneration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default template approach&lt;/strong&gt; (simplified):&lt;/p&gt;

&lt;p&gt;This is what the generated route looked like before my changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/flags"&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;// Your business logic goes HERE, in generated code&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateFlagRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// ...&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;I needed the generated code to define an interface and delegate to my implementation. So I can write business logic in my own files that don't get overwritten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My template approach&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;This is what the generated route looks like after my changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define an interface for handlers. My code implements this.&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FlagsApiHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;createFlag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Call this from my routing setup&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FlagsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FlagsApiHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/flags"&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createFlag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Delegates to MY code&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;The template that generates this (&lt;code&gt;api.mustache&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;interface &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;classname&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;Handler {
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;operations&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;operation&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        val &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;operationId&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;: suspend RoutingContext.() -&amp;gt; Unit
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;operation&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;operations&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
}

fun Route.&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;classname&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;(handlers: &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;classname&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;Handler) {
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;operation&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;hasAuthMethods&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    authenticate("&lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;") {
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;hasAuthMethods&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    route("&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;") {
        &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lowercase&lt;/span&gt;&lt;span class="k"&gt;}}{{&lt;/span&gt;&lt;span class="nv"&gt;httpMethod&lt;/span&gt;&lt;span class="k"&gt;}}{{/&lt;/span&gt;&lt;span class="nn"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lowercase&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;(handlers.&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;operationId&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;)
    }
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;hasAuthMethods&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    }
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;hasAuthMethods&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;operation&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mustache loops over &lt;code&gt;operations&lt;/code&gt; and &lt;code&gt;operation&lt;/code&gt; (provided by the generator). I template out the interface properties and route wiring. &lt;code&gt;{{#hasAuthMethods}}&lt;/code&gt; wraps routes in &lt;code&gt;authenticate()&lt;/code&gt; when the spec has security requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  The @Serializable Fix
&lt;/h3&gt;

&lt;p&gt;Ktor 2 uses kotlinx.serialization. Default templates assume Gson or Jackson. Generated DTOs wouldn't serialize.&lt;/p&gt;

&lt;p&gt;Fix in &lt;code&gt;data_class.mustache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;import kotlinx.serialization.Serializable

@Serializable
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;hasVars&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;data &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;hasVars&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;class &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;classname&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;(
    // ... fields
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it—unconditionally add the annotation. Every generated DTO now serializes correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vendor Extensions for Edge Cases
&lt;/h3&gt;

&lt;p&gt;Some fields need special handling that the spec can't express natively. OpenAPI supports vendor extensions (custom properties prefixed with &lt;code&gt;x-&lt;/code&gt;) that templates can read.&lt;/p&gt;

&lt;p&gt;My feature flag values can be any type: boolean, string, number, or object. In OpenAPI, that's &lt;code&gt;type: object&lt;/code&gt;, which generates &lt;code&gt;kotlin.Any&lt;/code&gt;. But kotlinx.serialization can't handle &lt;code&gt;Any&lt;/code&gt; without an explicit &lt;code&gt;@Contextual&lt;/code&gt; annotation.&lt;/p&gt;

&lt;p&gt;In the spec:&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;value&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;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Flag value (can be boolean, string, number, or object)&lt;/span&gt;
  &lt;span class="na"&gt;x-is-any-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# My custom extension&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the template (&lt;code&gt;data_class_req_var.mustache&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;vendorExtensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;x-is-any-type&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;@kotlinx.serialization.Contextual &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;vendorExtensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;x-is-any-type&lt;/span&gt;&lt;span class="k"&gt;}}{{&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;modelMutable&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;: &lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;dataType&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now fields marked with &lt;code&gt;x-is-any-type: true&lt;/code&gt; get the annotation they need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Generation Patching
&lt;/h3&gt;

&lt;p&gt;Some fixes can't be done in templates at all. Mustache is intentionally logic-less—it can output variables and loop over collections, but it can't do string manipulation or conditional type replacement.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;Map&amp;lt;String, Any&amp;gt;&lt;/code&gt; doesn't serialize with kotlinx.serialization. I need to replace it with &lt;code&gt;JsonObject&lt;/code&gt;. But the template just outputs &lt;code&gt;{{{dataType}}}&lt;/code&gt;—whatever the generator provides. There's no way to say "if dataType contains Map, output JsonObject instead."&lt;/p&gt;

&lt;p&gt;The solution: a Gradle task that patches generated files after generation but before compilation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"patchGeneratedDtos"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"openApiGenerate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;doLast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;generatedDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dto.kt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;// OffsetDateTime needs @Contextual for custom serializer&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""(?&amp;lt;!@Contextual )(val \w+: java\.time\.OffsetDateTime)"""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s"&gt;"@kotlinx.serialization.Contextual $1"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;// Map&amp;lt;String, Any&amp;gt;? can't serialize—replace with JsonObject?&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""(val \w+): kotlin\.collections\.Map&amp;lt;kotlin\.String, kotlin\.Any&amp;gt;\?"""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s"&gt;"$1: kotlinx.serialization.json.JsonObject?"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;named&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"compileKotlin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"patchGeneratedDtos"&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;This runs after generation but before compilation. It's not elegant, but it works reliably.&lt;/p&gt;

&lt;p&gt;Could I fix this upstream in OpenAPI Generator? It's open source. But it's a big codebase, the fix would need to handle all Kotlin serialization options, and I'd be signing up to maintain it. For two regex replacements, a post-processing script is fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping It All in Sync
&lt;/h2&gt;

&lt;p&gt;This only works if both generators actually run. I automated it so I can't forget.&lt;/p&gt;

&lt;p&gt;Frontend generator runs on &lt;code&gt;pnpm start&lt;/code&gt; and &lt;code&gt;pnpm build&lt;/code&gt; via pre-scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prestart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm run generate-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prebuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm run generate-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"generate-api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./scripts/generate-api.sh"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;api.yml&lt;/code&gt; and forget to regenerate? Starting the dev server catches it. TypeScript flags any breaking changes.&lt;/p&gt;

&lt;p&gt;Kotlin side runs generation as part of &lt;code&gt;./gradlew build&lt;/code&gt;, so it's always current.&lt;/p&gt;




&lt;h2&gt;
  
  
  On Teams
&lt;/h2&gt;

&lt;p&gt;PRs become about the spec, not the implementation. You're reviewing &lt;code&gt;api.yml&lt;/code&gt; changes—"is this the right shape?"—and once that's agreed on, the implementation is just implementation.&lt;/p&gt;

&lt;p&gt;Merge conflicts in YAML will happen. Thousands of lines, multiple people. You'd want conventions around who owns what, or split the spec into multiple files (&lt;code&gt;$ref&lt;/code&gt; supports this).&lt;/p&gt;

&lt;p&gt;I don't commit generated code—it regenerates on every build. That means the build itself enforces consistency. Change the spec, the generated interfaces change, your code won't compile until it matches. If you do commit generated code, you'd want CI to regenerate and fail on any diff.&lt;/p&gt;




&lt;h2&gt;
  
  
  When This Pattern Might Not Fit
&lt;/h2&gt;

&lt;p&gt;Spec-first has an upfront cost: learning Mustache templates, configuring generators, debugging serialization issues. Once it's working, the day-to-day is actually faster—less boilerplate than writing everything twice.&lt;/p&gt;

&lt;p&gt;But it might not be worth the setup if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same language everywhere&lt;/strong&gt;: TypeScript on both ends? Tools like tRPC give you type safety without the YAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny project&lt;/strong&gt;: Five endpoints, one developer? The generator setup might take longer than just writing the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team won't adopt it&lt;/strong&gt;: The benefits require everyone to follow the workflow. If people bypass the spec, you're back to drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It pays off most when you have multiple consumers, different languages, or a team that benefits from the spec as documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Got Out of It
&lt;/h2&gt;

&lt;p&gt;After 70 endpoints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero manual route definitions.&lt;/strong&gt; Every route comes from the spec. I write business logic; the generator handles wiring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile-time safety across languages.&lt;/strong&gt; Kotlin handler doesn't match the spec? Won't compile. TypeScript client uses a nonexistent field? Won't compile. Drift is impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation that can't rot.&lt;/strong&gt; The spec &lt;em&gt;is&lt;/em&gt; the documentation. Not a separate artifact that falls out of sync—it's the source that generates the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onboarding is easier.&lt;/strong&gt; New devs read the spec to understand the API. No reverse-engineering handler code scattered across files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLMs work better.&lt;/strong&gt; I point AI assistants at &lt;code&gt;api.yml&lt;/code&gt; and they immediately understand every endpoint, request shape, and response type. No ambiguity, no hunting through files. The spec is a perfect context document—structured, complete, always current. Single source of truth works for humans and machines.&lt;/p&gt;

&lt;p&gt;The upfront investment was real—custom templates, post-processing patches, learning generator quirks. But that was one-time. Now adding an endpoint is: update the YAML, implement the handler, use the client. The spec keeps everything honest.&lt;/p&gt;

&lt;p&gt;If you're building a multi-language stack and tired of keeping types in sync manually, worth a look.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Things I haven't done yet but probably should:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request validation.&lt;/strong&gt; The spec already defines constraints—required fields, string patterns, min/max values—but I'm not using them. I still validate manually in handlers. There are libraries that can validate requests against the spec automatically. I should probably use one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split the spec into multiple files.&lt;/strong&gt; 3,300 lines in one file is a lot. OpenAPI lets you &lt;code&gt;$ref&lt;/code&gt; external files, so I could break it up by domain: &lt;code&gt;auth.yml&lt;/code&gt;, &lt;code&gt;flags.yml&lt;/code&gt;, &lt;code&gt;targeting.yml&lt;/code&gt;. Just haven't gotten around to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated API docs.&lt;/strong&gt; Swagger UI, Redoc, that kind of thing. Once you have a spec, these are basically free. I just haven't set it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec-driven testing.&lt;/strong&gt; I'm not using the spec for testing yet, but I'd like to. &lt;a href="https://github.com/stoplightio/prism" rel="noopener noreferrer"&gt;Prism&lt;/a&gt; can mock the API from the spec—run frontend tests without the real backend. Contract testing can verify the server actually matches the spec. Could generate test fixtures from example responses. Lots of options I haven't explored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better error schemas.&lt;/strong&gt; My errors are just &lt;code&gt;mapOf("error" to message)&lt;/code&gt;. Kind of sloppy. Proper error schemas in the spec would make the frontend error handling cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Do You Think?
&lt;/h2&gt;

&lt;p&gt;Is this overkill? Writing YAML before code, customizing Mustache templates, patching generated files—it's a lot of machinery. Maybe you look at this and think "I'd rather just write the types twice."&lt;/p&gt;

&lt;p&gt;Fair. It's not for every project.&lt;/p&gt;

&lt;p&gt;But if you've felt the pain of API drift, or you're tired of keeping frontend types in sync with backend changes, maybe it's worth the setup. I'd be curious whether anyone else is doing something similar—or if you've tried it and decided it wasn't worth it.&lt;/p&gt;

&lt;p&gt;Would you use this? Let me know.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>openapi</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Self-hosted feature flags for Rails (with typed values)</title>
      <dc:creator>Lucas Dachman</dc:creator>
      <pubDate>Sun, 14 Dec 2025 02:24:32 +0000</pubDate>
      <link>https://dev.to/lucasdachman/self-hosted-feature-flags-for-rails-with-typed-values-42ng</link>
      <guid>https://dev.to/lucasdachman/self-hosted-feature-flags-for-rails-with-typed-values-42ng</guid>
      <description>&lt;p&gt;I built a feature flag gem for Rails that stores flags in your own database. No external service needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I wanted feature flags that return more than just true/false. Sometimes you need a number (rate limits), a string (welcome message), or JSON (config object). &lt;/p&gt;

&lt;p&gt;Flipper is boolean-only. The bigger tools (LaunchDarkly, etc.) felt like overkill and I didn't want to depend on an external service.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"subflag-rails"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails generate subflag:install &lt;span class="nt"&gt;--backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;active_record
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Subflag&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engine&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/subflag"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You get an admin UI at &lt;code&gt;/subflag&lt;/code&gt; to manage flags.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fwloc22esqzd6iyqii6po.png" class="article-body-image-wrapper"&gt;&lt;img src="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%2Fwloc22esqzd6iyqii6po.png" alt="Admin UI" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Typed values, not just booleans&lt;/span&gt;
&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subflag_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:max_uploads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subflag_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:pricing_tiers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;free: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pro: &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;welcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subflag_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:welcome_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Booleans work too&lt;/span&gt;
&lt;span class="n"&gt;subflag_enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:new_checkout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  User targeting
&lt;/h2&gt;

&lt;p&gt;Show different values to different users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/subflag.rb&lt;/span&gt;
&lt;span class="no"&gt;Subflag&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:active_record&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_context&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;targeting_key: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;plan: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your targeting rules, you can say "users with plan=pro get max_uploads=100" while everyone else gets 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud option
&lt;/h2&gt;

&lt;p&gt;There's also a hosted version at &lt;a href="https://subflag.com" rel="noopener noreferrer"&gt;subflag.com&lt;/a&gt; if you want a dashboard, multiple environments, and percentage rollouts. Same API either way.&lt;/p&gt;




&lt;p&gt;GitHub: &lt;a href="https://github.com/subflag/sdk/tree/main/packages/subflag-rails" rel="noopener noreferrer"&gt;https://github.com/subflag/sdk/tree/main/packages/subflag-rails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback if you try it.&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>rails</category>
      <category>opensource</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
