<?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: tkode</title>
    <description>The latest articles on DEV Community by tkode (@tkode_dev).</description>
    <link>https://dev.to/tkode_dev</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%2F3921100%2F2cb937d9-f605-44fe-a04a-f2f151a0cc9e.png</url>
      <title>DEV Community: tkode</title>
      <link>https://dev.to/tkode_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tkode_dev"/>
    <language>en</language>
    <item>
      <title>I let my OpenAPI spec do the work: one contract for Go, Flutter, and the LLM</title>
      <dc:creator>tkode</dc:creator>
      <pubDate>Sat, 09 May 2026 16:54:08 +0000</pubDate>
      <link>https://dev.to/tkode_dev/i-let-my-openapi-spec-do-the-work-one-contract-for-go-flutter-and-the-llm-c20</link>
      <guid>https://dev.to/tkode_dev/i-let-my-openapi-spec-do-the-work-one-contract-for-go-flutter-and-the-llm-c20</guid>
      <description>&lt;p&gt;There was a period on a Django project where a simple question of &lt;em&gt;"is this field actually used on the frontend?"&lt;/em&gt; would trigger a small investigation. You'd check the backend, grep through the React code, and ask someone who might know. The answer was always in there somewhere, but it was never in one place. Not like anyone was being careless. There just wasn't a contract. So every question got answered by digging.&lt;/p&gt;

&lt;p&gt;My stack is Go on the backend and Flutter on the frontend. Before I settled on the current approach, adding an endpoint meant the same work twice. Define request and response structs in Go, wire the handler, add middleware. Then repeat the whole model layer in Dart, and build a repository around Dio to actually call it. Every change paid the same tax. Low-grade, but it compounds.&lt;/p&gt;

&lt;p&gt;The fix I landed on isn't novel in isolation: generate code from an OpenAPI spec. But the way it fits together took a few projects to get right. The spec isn't documentation. It's the artifact everything compiles against — and the layer where human judgment, compiler enforcement, and LLM reasoning all meet before any implementation happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  The insight: the spec isn't documentation, it's the artifact
&lt;/h2&gt;

&lt;p&gt;Most OpenAPI content treats the spec as something you generate from code or maintain alongside it. The approach I kept arriving at, across three projects, was the opposite: &lt;strong&gt;write the spec first, generate everything from it, and never let code drift from it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This works because OpenAPI is structured, checkable, and well-represented in LLM training data. When I ask a model to design an endpoint, it writes OpenAPI YAML reliably — better than it writes idiomatic Go or Dart from scratch. That makes the spec a natural handoff point: the human reviews it, the compiler enforces it, and the LLM never touches implementation until the contract is settled.&lt;/p&gt;

&lt;p&gt;The first project where I tried this properly was in Flutter. Dart is typed by default, and the friction of keeping Go structs and Dart models manually aligned was obvious from the start. I set up code generation on both sides, and the gap closed. When I started the current project (an institute ERP), this was the natural starting point, not an experiment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The codegen pipeline
&lt;/h2&gt;

&lt;p&gt;The monorepo structure matters here. Everything lives together because the spec has to be the single source of truth across consumers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api/
├── services/
│   ├── auth.yaml
│   ├── students.yaml
│   ├── attendance.yaml
│   ├── fees.yaml
│   └── ... (23 service files total)
├── openapi.yaml          ← combined, generated by redocly
├── index/                ← auto-generated navigation for LLMs
├── redocly.yaml
└── scripts/
    └── post-build.js

backend/gen/api/
├── attendance/
│   ├── codegen.yaml
│   ├── generate.go
│   └── schema.gen.go
├── auth/
│   ├── codegen.yaml
│   ├── generate.go
│   └── schema.gen.go
├── fees/
│   ├── codegen.yaml
│   ├── generate.go
│   └── schema.gen.go
└── ... (23 services, mirroring the spec split)

frontend/packages/shared_api_client/lib/
├── attendance/
│   ├── attendance_client.dart        ← generated
│   └── attendance_client.g.dart      ← generated
├── models/
│   ├── attendance_status.dart
│   ├── get_today_attendance_response.dart
│   ├── get_today_attendance_response.freezed.dart
│   ├── get_today_attendance_response.g.dart
│   ├── today_attendance_item.dart
│   ├── today_attendance_item.freezed.dart
│   └── today_attendance_item.g.dart
│   └── ... (400+ model files across all services)
└── export.dart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service gets its own YAML file. &lt;code&gt;openapi.yaml&lt;/code&gt; is assembled from these by redocly. A &lt;code&gt;post-build.js&lt;/code&gt; script handles two things after the build: setting the correct server URLs for hosted documentation, and stripping a handful of empty fields that break swagger-parser downstream. That second task isn't something glamorous, but is a real part of any pipeline like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend.&lt;/strong&gt; &lt;a href="https://github.com/oapi-codegen/oapi-codegen" rel="noopener noreferrer"&gt;oapi-codegen&lt;/a&gt; generates Go interfaces and types from the spec. The backend mirrors the service split: &lt;code&gt;backend/gen/api/&lt;/code&gt; has a folder per service, each with a config, a &lt;code&gt;generate.go&lt;/code&gt;, and a &lt;code&gt;schema.gen.go&lt;/code&gt;. I wire these to gin handlers and implement the logic in &lt;code&gt;backend/src/&lt;/code&gt;, same folder structure. Running &lt;code&gt;go generate&lt;/code&gt; regenerates the backend code when the spec changes. It's intentionally kept as a manual trigger. You run it when you've made a deliberate change to the spec, not on every save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend.&lt;/strong&gt; The combined &lt;code&gt;openapi.yaml&lt;/code&gt; feeds into swagger-parser with a configuration that splits output by OpenAPI tags and generates frozen Dart classes via freezed. The generated code lives in a &lt;code&gt;shared_api&lt;/code&gt; package that both the institute-facing and parent-facing apps consume. Dart tree-shaking handles unused paths.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;components/schemas&lt;/code&gt; section from &lt;code&gt;attendance.yaml&lt;/code&gt; (API paths excluded for brevity):&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;AttendanceStatus&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;string&lt;/span&gt;
  &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PRESENT&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ABSENT&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NOT_SET&lt;/span&gt;

&lt;span class="na"&gt;TodayAttendanceItem&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="s"&gt;studentId&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;studentName&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admissionNumber&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rollNumber&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;studentId&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;string&lt;/span&gt;
      &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uuid&lt;/span&gt;
      &lt;span class="na"&gt;x-go-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uuid.UUID&lt;/span&gt;
      &lt;span class="na"&gt;x-go-type-import&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/google/uuid&lt;/span&gt;
    &lt;span class="na"&gt;studentName&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;string&lt;/span&gt;
    &lt;span class="na"&gt;admissionNumber&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;string&lt;/span&gt;
    &lt;span class="na"&gt;rollNumber&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;string&lt;/span&gt;
    &lt;span class="na"&gt;status&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/AttendanceStatus'&lt;/span&gt;
    &lt;span class="na"&gt;remarks&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;string&lt;/span&gt;

&lt;span class="na"&gt;GetTodayAttendanceResponse&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="s"&gt;date&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;items&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;totalCount&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;date&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;string&lt;/span&gt;
      &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-03-21'&lt;/span&gt;
    &lt;span class="na"&gt;items&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;array&lt;/span&gt;
      &lt;span class="na"&gt;items&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/TodayAttendanceItem'&lt;/span&gt;
    &lt;span class="na"&gt;totalCount&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;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated Go (&lt;code&gt;schema.gen.go&lt;/code&gt;):&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%2Fphfcuut7arzscw8crbjs.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%2Fphfcuut7arzscw8crbjs.png" alt="Generated Go Code" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generated Dart (&lt;code&gt;shared_api&lt;/code&gt; package):&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%2F7hb03s6vu37bgiz3m4eo.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%2F7hb03s6vu37bgiz3m4eo.png" alt="Generated Dart Code" width="800" height="828"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;x-go-type&lt;/code&gt; extension on &lt;code&gt;studentId&lt;/code&gt; tells the generator to use &lt;code&gt;uuid.UUID&lt;/code&gt; instead of plain &lt;code&gt;string&lt;/code&gt;. The Dart side doesn't see it; it gets &lt;code&gt;String&lt;/code&gt;, which is correct. One spec, two different type systems, no compromise.&lt;/p&gt;

&lt;p&gt;The enforcement layer is the LSP. If the spec changes and codegen runs, any mismatch between how the backend implements an interface and what the spec defines is a type error, caught before the code runs. Same on the Flutter side. It's not a document you can ignore. It's a thing the compiler checks.&lt;/p&gt;

&lt;p&gt;The tooling is table stakes. Here's why this setup pays off specifically when working with an LLM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agentic coding is why this is worth the setup cost
&lt;/h2&gt;

&lt;p&gt;I use agentic coding heavily on this project. The spec-first approach and the agentic approach reinforce each other in a way that's hard to see until you've hit the alternative.&lt;/p&gt;

&lt;p&gt;Without a contract, LLM-generated backend and LLM-generated frontend code drift silently. The model that wrote your Go handler and the model that wrote your Dart repository have no shared ground truth. They make different assumptions about field names, nullable types, error shapes. You find out at runtime, or you don't find out until a user does.&lt;/p&gt;

&lt;p&gt;With codegen, drift becomes a compile error.&lt;/p&gt;

&lt;p&gt;The loop looks like this: I describe the feature I want, then explicitly ask the agent to update the relevant YAML files first — and nothing else. I review the spec change before proceeding. Only once the contract looks right do I tell the agent to run &lt;code&gt;go generate&lt;/code&gt; and continue to the backend implementation and tests. The frontend client is regenerated after the backend is stable — same script, pointed at the combined &lt;code&gt;openapi.yaml&lt;/code&gt;. At every step, the generated types are what both sides compile against. If something doesn't line up, the build fails, not the app.&lt;/p&gt;

&lt;p&gt;This sequencing is a discipline I maintain deliberately, not an automated constraint. You could enforce it via a CLAUDE.md instruction and get the same result. But the point is the same either way: the human reviews the contract before any implementation happens.&lt;/p&gt;

&lt;p&gt;The Flutter side took more iteration to get right, but the principle was the same: let the spec own the types, and let the compiler enforce it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The index layer: making the truth accessible to an agent efficiently
&lt;/h2&gt;

&lt;p&gt;The spec grew. It's now 23 service files, covering everything from auth to timetable to parent-facing dashboards. Without explicit instructions, an LLM working on a feature would load the entire combined &lt;code&gt;openapi.yaml&lt;/code&gt; — large, token-expensive, and mostly irrelevant. Pointing it to service-level specs helped, but only partially. The larger service files are 1000+ lines, and even those contain models unrelated to the task at hand. The agent still had no way to navigate to just what it needed.&lt;/p&gt;

&lt;p&gt;So I built a lightweight navigation layer, auto-generated by &lt;a href="https://gist.github.com/tkode-dev/76900b98fdf98fac16b15c8e9ec419a2" rel="noopener noreferrer"&gt;a Python script&lt;/a&gt; and kept up to date via a pre-commit hook. It lives in &lt;code&gt;api/index/&lt;/code&gt; and produces three things: a top-level README with a service summary table; one detail file per service listing its endpoints and the models they use; and a combined &lt;code&gt;models.md&lt;/code&gt; covering every model across all services, so an agent can search by model name without knowing which service file to open.&lt;/p&gt;

&lt;p&gt;The entry point is a README that gives a high-level service table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Operations&lt;/th&gt;
&lt;th&gt;Backend owner hints&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admission.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;large (1795)&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;backend/src/admission/&lt;/td&gt;
&lt;td&gt;Admission Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attendance.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;medium (561)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;backend/src/attendance/&lt;/td&gt;
&lt;td&gt;Attendance Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;institute.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;small (237)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;backend/src/institute/&lt;/td&gt;
&lt;td&gt;Institute Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;students.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;large (1760)&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;backend/src/students/&lt;/td&gt;
&lt;td&gt;Students Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The guidance in the README: small specs are cheap to read whole, large specs should be read by operation. Each service links to a detail file with an operation table and model index:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Operation ID&lt;/th&gt;
&lt;th&gt;Request models&lt;/th&gt;
&lt;th&gt;Response models&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/auth/onboarding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;submitInstituteOnboarding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OnboardingRequest&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt;: User, &lt;code&gt;400/401/409/500&lt;/code&gt;: ErrorResponse&lt;/td&gt;
&lt;td&gt;Submit institute details (Onboarding Stage 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/auth/me/institute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateCurrentUserInstitute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UpdateCurrentUserInstituteRequest&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt;: User, &lt;code&gt;400/401/403/404/500&lt;/code&gt;: ErrorResponse&lt;/td&gt;
&lt;td&gt;Update current user's institute details&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An agent looking at a narrow task — say, updating the fees endpoint — doesn't need to load &lt;code&gt;auth.yaml&lt;/code&gt; or &lt;code&gt;students.yaml&lt;/code&gt;. It looks at the index, navigates to &lt;code&gt;fees.md&lt;/code&gt;, finds the operation, and loads only what it needs.&lt;/p&gt;

&lt;p&gt;It's still rough in places — the size classification (&lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large&lt;/code&gt;) is a heuristic, not a formal metric. But a rough heuristic maintained automatically is more reliable than a precise one maintained manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-developer friction.&lt;/strong&gt; Frontend and backend for a feature have to move together because they share generated types. When you're solo, that's fine. On a team, parallel work on the same service generates conflicts in generated files, and onboarding someone new requires a conversation before they can be productive. There are mitigations (per-service generated clients, stricter spec ownership boundaries), but I haven't needed to solve this yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec evolution.&lt;/strong&gt; When an endpoint changes shape: update the spec, regenerate, fix the compile errors. The compile errors are useful: they surface exactly where something is breaking, which sometimes makes it obvious that old clients would be affected. Once that led me to mark a field as deprecated and add a new field alongside it rather than rename in place. The weak part is observability. There's no reliable way to know if old client versions in the wild are still hitting deprecated fields. For this project that's fine, as it's pre-launch. But worth being honest about if you're running something in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The swagger-parser error type limitation.&lt;/strong&gt; The Flutter codegen only produces one response type per endpoint: the success type. All error codes get parsed manually. I've accepted this for now. It could be addressed by extending swagger-parser, but it hasn't been worth the time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The post-build patching.&lt;/strong&gt; The fact that &lt;code&gt;post-build.js&lt;/code&gt; exists to strip empty fields that break downstream tooling is a real thing. The pipeline works, but it has seams. If you adopt this approach, you'll find your own seams. Budget for them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is actually about
&lt;/h2&gt;

&lt;p&gt;This is a workflow tradeoff: upfront structure in exchange for eliminating a specific category of mechanical toil. The toil it eliminates is model drift: the silent divergence between what your backend says and what your frontend expects. In an agentic workflow, that divergence happens faster and is harder to catch. The spec doesn't prevent LLMs from making mistakes. It just means a certain class of them — type mismatches, missing fields, inconsistent response shapes — become compile errors instead of bugs.&lt;/p&gt;

&lt;p&gt;The core of it — one spec, three consumers, everything compiled against the same ground truth — has been stable across multiple projects. That stability is what I was actually looking for. The index layer and the error type handling are still evolving; I'll write about those when there's more to say.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/oapi-codegen/oapi-codegen" rel="noopener noreferrer"&gt;oapi-codegen&lt;/a&gt; — Go code generation from OpenAPI specs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://youtu.be/87au30fl5e4" rel="noopener noreferrer"&gt;package main: Practical OpenAPI in Go&lt;/a&gt; — the video that started the Go side of this for me&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openapi</category>
      <category>go</category>
      <category>ai</category>
      <category>flutter</category>
    </item>
  </channel>
</rss>
