<?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: Tran Long An</title>
    <description>The latest articles on DEV Community by Tran Long An (@tranlongan).</description>
    <link>https://dev.to/tranlongan</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%2F1261450%2Ffade275d-19ff-489b-9008-2d6605e159a2.png</url>
      <title>DEV Community: Tran Long An</title>
      <link>https://dev.to/tranlongan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tranlongan"/>
    <language>en</language>
    <item>
      <title>Your Go struct already describes your API. Let it write the docs too.</title>
      <dc:creator>Tran Long An</dc:creator>
      <pubDate>Sun, 07 Jun 2026 04:04:20 +0000</pubDate>
      <link>https://dev.to/tranlongan/your-go-struct-already-describes-your-api-let-it-write-the-docs-too-hf6</link>
      <guid>https://dev.to/tranlongan/your-go-struct-already-describes-your-api-let-it-write-the-docs-too-hf6</guid>
      <description>&lt;h1&gt;
  
  
  Your Go struct already describes your API. Let it write the docs too.
&lt;/h1&gt;

&lt;p&gt;If you build HTTP APIs in Go, you've lived this: the same endpoint is described in&lt;br&gt;
three different places, by hand, and all three have to agree.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The handler&lt;/strong&gt; reads the body, pulls query params, parses headers — binding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The validator&lt;/strong&gt; checks the body is well-formed — &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OpenAPI doc&lt;/strong&gt; tells clients what the endpoint expects and returns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code already &lt;em&gt;knows&lt;/em&gt; all of this. The Go type for the request says exactly&lt;br&gt;
which fields exist, what types they are, which are required. Yet the docs repeat&lt;br&gt;
it in YAML, the validator repeats it in tags, and nothing keeps them in sync. You&lt;br&gt;
rename a field, ship it, and the docs quietly lie until someone files a bug.&lt;/p&gt;

&lt;p&gt;Multiply that by a few dozen routes, each wired slightly differently across&lt;br&gt;
&lt;code&gt;gin&lt;/code&gt;, &lt;code&gt;chi&lt;/code&gt;, or &lt;code&gt;net/http&lt;/code&gt;, and "the docs" become a second codebase you maintain&lt;br&gt;
by discipline alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;oapi&lt;/code&gt; removes two of those three places.&lt;/strong&gt; You write the request and response&lt;br&gt;
as typed Go structs once. Binding, validation, and the OpenAPI 3 document all read&lt;br&gt;
the &lt;em&gt;same&lt;/em&gt; struct tags — so the docs are generated from the exact types your&lt;br&gt;
handler binds. They cannot drift, because there is nothing to keep in sync.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/antlss/oapi" rel="noopener noreferrer"&gt;github.com/antlss/oapi&lt;/a&gt; — &lt;code&gt;go get github.com/antlss/oapi&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The idea in one signature
&lt;/h2&gt;

&lt;p&gt;Every handler has the same shape: a typed request in, a typed response out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Body&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="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;Request[Header, Param, Query, Body]&lt;/code&gt; is the whole contract. Each part binds from&lt;br&gt;
a different source, and you use &lt;code&gt;struct{}&lt;/code&gt; for the parts an endpoint doesn't need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Header -&amp;gt; `header:"..."`   Param -&amp;gt; `uri:"..."`
Query  -&amp;gt; `form:"..."`     Body  -&amp;gt; `json:"..."` (or `form:"..."` for multipart/urlencoded)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The handler receives data already parsed, validated, and typed. No&lt;br&gt;
&lt;code&gt;c.ShouldBindJSON(&amp;amp;x)&lt;/code&gt;, no manual &lt;code&gt;c.Param("id")&lt;/code&gt;, no &lt;code&gt;if err != nil&lt;/code&gt; boilerplate&lt;br&gt;
in every function.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the type buys you
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. The type is the entire data model
&lt;/h3&gt;

&lt;p&gt;Define the request once. The struct tags carry three readers at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CreateProductBody&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"name"      binding:"required,min=2,max=120"                   example:"Mechanical Keyboard"`&lt;/span&gt;
    &lt;span class="n"&gt;SKU&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"sku"       binding:"required,uuid"                            example:"5f9c2e3a-1b4d-4c8e-9f0a-2b3c4d5e6f70"`&lt;/span&gt;
    &lt;span class="n"&gt;Price&lt;/span&gt;     &lt;span class="kt"&gt;float64&lt;/span&gt;  &lt;span class="s"&gt;`json:"price"     binding:"required,gt=0"                            example:"49.90"`&lt;/span&gt;
    &lt;span class="n"&gt;Currency&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"currency"  binding:"required,oneof=USD EUR JPY VND"           example:"USD"`&lt;/span&gt;
    &lt;span class="n"&gt;Category&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"category"  binding:"required,oneof=book electronics food toy" example:"electronics"`&lt;/span&gt;
    &lt;span class="n"&gt;Website&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"website"   binding:"omitempty,url"                            example:"https://example.com"`&lt;/span&gt;
    &lt;span class="n"&gt;Tags&lt;/span&gt;      &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"tags"      binding:"omitempty,max=10"                         example:"new,featured"`&lt;/span&gt;
    &lt;span class="n"&gt;Warehouse&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt;  &lt;span class="s"&gt;`json:"warehouse"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;json:"name"&lt;/code&gt; — how the body decodes (binding).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;binding:"required,min=2,max=120"&lt;/code&gt; — what makes it valid, &lt;strong&gt;and&lt;/strong&gt; the schema
constraints in the docs (required field, &lt;code&gt;minLength&lt;/code&gt;/&lt;code&gt;maxLength&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;example:"..."&lt;/code&gt; — the sample value clients see in Swagger UI / Redoc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nested structs like &lt;code&gt;Warehouse Address&lt;/code&gt; are recursed into — their rules and&lt;br&gt;
examples reach the docs too. Rename &lt;code&gt;Name&lt;/code&gt;, change &lt;code&gt;min=2&lt;/code&gt; to &lt;code&gt;min=3&lt;/code&gt;, add a&lt;br&gt;
field: the binding, the validation, and the published schema all move together,&lt;br&gt;
because they &lt;em&gt;are&lt;/em&gt; the same declaration.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The docs generate themselves — and stay honest
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;Registry&lt;/code&gt; collects your routes and turns them into a validated OpenAPI 3&lt;br&gt;
document (JSON or YAML). Because it reads the captured Go types, not a separate&lt;br&gt;
spec file, the document describes exactly what the handler binds.&lt;/p&gt;

&lt;p&gt;Here is the struct above, rendered with zero hand-written OpenAPI:&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%2Fxmx39drb7ipcdb0bo58o.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%2Fxmx39drb7ipcdb0bo58o.png" alt=" " width="800" height="436"&gt;&lt;/a&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%2F5we8pbbueu517mojo82m.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%2F5we8pbbueu517mojo82m.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice what carried over automatically: &lt;code&gt;category&lt;/code&gt; shows its &lt;code&gt;oneof&lt;/code&gt; as an&lt;br&gt;
&lt;strong&gt;enum&lt;/strong&gt;, &lt;code&gt;sku&lt;/code&gt; is documented as a &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; shows its &lt;code&gt;[2..120] characters&lt;/code&gt;&lt;br&gt;
bound, &lt;code&gt;website&lt;/code&gt; as a &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; as an array with &lt;code&gt;&amp;lt;= 10 items&lt;/code&gt;, and the&lt;br&gt;
request sample uses your real &lt;code&gt;example&lt;/code&gt; values instead of bare &lt;code&gt;"string"&lt;/code&gt;&lt;br&gt;
placeholders. None of that was written by hand. It was read off the type.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Standardized requests and responses — across five frameworks
&lt;/h3&gt;

&lt;p&gt;The same &lt;code&gt;[]Route&lt;/code&gt; runs unchanged on &lt;strong&gt;net/http&lt;/strong&gt;, &lt;strong&gt;gin&lt;/strong&gt;, &lt;strong&gt;Fiber v2&lt;/strong&gt;,&lt;br&gt;
&lt;strong&gt;chi&lt;/strong&gt;, and &lt;strong&gt;Echo v4&lt;/strong&gt;. The core is framework-agnostic; each adapter is a thin&lt;br&gt;
seam. Pick a framework, or switch later, without rewriting a single handler.&lt;/p&gt;

&lt;p&gt;Successful responses share one envelope by default — &lt;code&gt;{"data": ...}&lt;/code&gt;, plus&lt;br&gt;
&lt;code&gt;{"meta": ...}&lt;/code&gt; for paginated endpoints — so every endpoint in your API answers&lt;br&gt;
in a predictable shape, and clients can rely on it.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. You still own the parts that are opinions
&lt;/h3&gt;

&lt;p&gt;Standardization shouldn't mean a straitjacket. The things every project does&lt;br&gt;
differently are &lt;strong&gt;pluggable seams&lt;/strong&gt;, and the library ships no policy of its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validation&lt;/strong&gt; is an interface. The core depends on no validation library. Plug
in &lt;code&gt;go-playground/validator&lt;/code&gt; (a ready reference implementation is included), or
your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The response envelope&lt;/strong&gt; is swappable. Keep the default &lt;code&gt;{"data": ...}&lt;/code&gt;, switch
to &lt;code&gt;{"success": true, "data": ...}&lt;/code&gt; for the whole API, override it per route, or
return the raw model with no wrapper at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; is yours. Return an &lt;code&gt;HTTPError&lt;/code&gt;, map domain errors per route
with an &lt;code&gt;ErrorMapper&lt;/code&gt;, or install one process-wide &lt;code&gt;ErrorParser&lt;/code&gt; that renders
every error in your project's shape — and &lt;em&gt;that shape gets documented too&lt;/em&gt;.
Anything unrecognized renders a generic 500 and never leaks internals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, every one of these seams drives &lt;strong&gt;both&lt;/strong&gt; the bytes on the wire &lt;strong&gt;and&lt;/strong&gt;&lt;br&gt;
the generated docs. Customize the error shape, and the OpenAPI document describes&lt;br&gt;
your custom error shape. No drift, even when you go off the defaults.&lt;/p&gt;


&lt;h2&gt;
  
  
  A complete API in a few lines
&lt;/h2&gt;

&lt;p&gt;Define a handler over a typed request, declare the route with whatever&lt;br&gt;
documentation metadata you want, and mount it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;createProduct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRichRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;CreateProductBody&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="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// req.Body is already bound + validated&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDataResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
                &lt;span class="n"&gt;WithStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
                &lt;span class="n"&gt;WithHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/products/%d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Create a product"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"catalog"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithSuccessStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithResponseType&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;](),&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithSecurity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bearerAuth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"products:write"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithResponse&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}](&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusConflict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Duplicate SKU"&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;Collect your routes into a &lt;code&gt;Registry&lt;/code&gt;, add document-level metadata once, and you&lt;br&gt;
have a self-describing API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Catalog API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A demo API exercising typed binding, validation-driven docs, files, paging, security and the full error model."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;AddServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Local server"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;AddSecurityScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bearerAuth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BearerAuth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;AddTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"catalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Browse, create and manage products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&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 wire it to a framework and serve the spec — here on &lt;code&gt;gin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// turn on validation; the core ships none&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ginadapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/openapi.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ginadapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpecHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c"&gt;// serve Swagger UI / Redoc from the same spec...&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or generate the spec to disk for CI, client codegen, or publishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;oapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GenConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Dir&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"./openapi"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c"&gt;// openapi.json + openapi.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole loop: write the type, write the handler, get a validated API&lt;br&gt;
&lt;em&gt;and&lt;/em&gt; its documentation. The screenshots above are this exact code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;Documentation drifts because it's a copy. The moment your API description lives in&lt;br&gt;
a separate file maintained by hand, it starts aging the second you ship a change.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;oapi&lt;/code&gt; makes the Go type the single source of truth. Binding reads it, validation&lt;br&gt;
reads it, the OpenAPI document reads it — one declaration, three jobs, no copies to&lt;br&gt;
keep in sync. You keep full control over the parts that are genuinely your call:&lt;br&gt;
how you validate, how you shape responses, how you handle errors. The library just&lt;br&gt;
makes sure that whatever you decide, the docs say the same thing your code does.&lt;/p&gt;

&lt;p&gt;Write the struct. Ship the API. The docs are already correct.&lt;/p&gt;




&lt;p&gt;&lt;code&gt;oapi&lt;/code&gt; is open source (MIT), pre-1.0, and lives on GitHub:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/antlss/oapi" rel="noopener noreferrer"&gt;github.com/antlss/oapi&lt;/a&gt;&lt;/strong&gt; — stars, issues and PRs&lt;br&gt;
are very welcome. Install with &lt;code&gt;go get github.com/antlss/oapi&lt;/code&gt;. Five adapters:&lt;br&gt;
net/http, gin, Fiber v2, chi, Echo v4. The runnable Catalog API that produced the&lt;br&gt;
screenshots above lives in &lt;a href="//../examples/"&gt;&lt;code&gt;examples/&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;go run ./examples/cmd/gin&lt;/code&gt;&lt;br&gt;
and open &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>swagger</category>
      <category>apidoc</category>
      <category>gencode</category>
    </item>
    <item>
      <title>I got tired of AI Reviewers hallucinating, so I built an Autonomous Agent for GitLab instead.</title>
      <dc:creator>Tran Long An</dc:creator>
      <pubDate>Sun, 08 Mar 2026 06:00:34 +0000</pubDate>
      <link>https://dev.to/tranlongan/i-got-tired-of-ai-reviewers-hallucinating-so-i-built-an-autonomous-agent-for-gitlab-instead-jeh</link>
      <guid>https://dev.to/tranlongan/i-got-tired-of-ai-reviewers-hallucinating-so-i-built-an-autonomous-agent-for-gitlab-instead-jeh</guid>
      <description>&lt;p&gt;We've all been there. You push a massive 300-line Merge Request, and within 3 seconds, an "AI Code Reviewer" bot leaves 12 comments on your PR.&lt;/p&gt;

&lt;p&gt;You read them. Two comments complain about a "missing import" that is clearly handled globally by your framework. One suggests a library you explicitly removed last sprint. The rest are annoying formatting nits. You quietly click "Resolve Thread" 12 times and sigh.&lt;/p&gt;

&lt;p&gt;Standard AI reviewers pipe your &lt;code&gt;.patch&lt;/code&gt; file into an LLM and pray for the best. &lt;strong&gt;They lack context.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s exactly why I built &lt;strong&gt;&lt;a href="https://github.com/antlss/gitlab-review-agent" rel="noopener noreferrer"&gt;AI Review Agent&lt;/a&gt;&lt;/strong&gt;. A truly autonomous, context-aware code review agent built natively for GitLab using Go.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 The Problem with "Dumb" AI Reviewers
&lt;/h3&gt;

&lt;p&gt;When a human senior developer reviews a PR, they don’t just look at the highlighted diff. If you modify a function signature, they Cmd+Click to see where else it's used. If you introduce a generic struct, they read the interface definition in another file. They &lt;em&gt;think&lt;/em&gt; before they speak.&lt;/p&gt;

&lt;p&gt;Most open-source AI reviewers can't do this. They suffer from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Codebase Context:&lt;/strong&gt; "Why did you use &lt;code&gt;mylog.Info()&lt;/code&gt; instead of &lt;code&gt;fmt.Println()&lt;/code&gt;?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Window Explosions:&lt;/strong&gt; Bombarding the LLM with a 150-file MR diff until it forgets your original system prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-Way Communication:&lt;/strong&gt; They dump a review and vanish. You can't ask them to elaborate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🛠️ How AI Review Agent Fixes This
&lt;/h3&gt;

&lt;p&gt;I wanted to build an agent that behaves like an actual colleague. Here is how its pipeline works differently:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Agentic Tool Use (The Secret Sauce)
&lt;/h4&gt;

&lt;p&gt;The bot doesn't just statically read the diff. It's equipped with tools like &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;search_code&lt;/code&gt;, and &lt;code&gt;multi_diff&lt;/code&gt;.&lt;br&gt;
If it sees you calling a mysterious &lt;code&gt;CacheManager.Get()&lt;/code&gt;, it will pause, use &lt;code&gt;search_code&lt;/code&gt; to find the &lt;code&gt;CacheManager&lt;/code&gt; implementation in the codebase, read it, and &lt;em&gt;then&lt;/em&gt; decide if your code is buggy. No more hallucinated assertions.&lt;/p&gt;
&lt;h4&gt;
  
  
  2. The Interactive Reply Loop 💬
&lt;/h4&gt;

&lt;p&gt;Most bots drop a comment and disappear. &lt;strong&gt;AI Review Agent stays in the conversation.&lt;/strong&gt;&lt;br&gt;
If the AI leaves a comment on your code, you can literally &lt;code&gt;@reply&lt;/code&gt; to it on the GitLab thread.&lt;br&gt;
&lt;em&gt;"Actually, I did it this way because of a race condition in the upstream service."&lt;/em&gt;&lt;br&gt;
A dedicated Replier Agent wakes up, reads the entire thread history, analyzes the surrounding code context again, and continues the technical debate. It will either apologize and agree with you, or push back if it finds a genuine flaw in your logic.&lt;/p&gt;
&lt;h4&gt;
  
  
  3. It Actually Learns From Your Team 📈
&lt;/h4&gt;

&lt;p&gt;Every codebase has its own unwritten rules. AI Review Agent is designed to get smarter over time.&lt;br&gt;
It features a background Cron job (Feedback Consolidator) that periodically scans historical human replies and resolved AI comments across your GitLab projects. It extracts "lessons learned" and builds a cached &lt;strong&gt;"Repository Best Practices"&lt;/strong&gt; rulebook.&lt;br&gt;
If your team agreed on a specific logging format last month, the agent remembers it and enforces it on all future PRs.&lt;/p&gt;
&lt;h4&gt;
  
  
  4. Seamless Webhook &amp;amp; Background Worker Server ⚙️
&lt;/h4&gt;

&lt;p&gt;It’s not just a script you run manually. The AI Review Agent operates as a high-performance &lt;strong&gt;Webhook Server&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You configure it on your GitLab project once.&lt;/li&gt;
&lt;li&gt;Every time a developer pushes code, the webhook triggers the agent.&lt;/li&gt;
&lt;li&gt;Reviews are pushed asynchronously into an intelligent Queue / Worker Pool system with retry logic. So even if the OpenAI API hiccups, your code review is never lost.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  5. Interactive CLI (Dry Run Mode)
&lt;/h4&gt;

&lt;p&gt;Are you scared of installing a bot that might spam your entire team on GitLab?&lt;br&gt;
You can run AI Review Agent locally against a live PR via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./cli review &lt;span class="nt"&gt;--project-id&lt;/span&gt; 123 &lt;span class="nt"&gt;--mr-id&lt;/span&gt; 45 &lt;span class="nt"&gt;--model&lt;/span&gt; claude-3-7-sonnet-20250219
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints the AI's suggestions directly in your terminal, and lets you interactively type &lt;code&gt;1, 3, 5&lt;/code&gt; to decide exactly which comments are worth pushing to the live GitLab MR.&lt;/p&gt;

&lt;h3&gt;
  
  
  🏗️ Built with Go (Clean Architecture)
&lt;/h3&gt;

&lt;p&gt;Under the hood, it’s built entirely in Go 1.25. It uses standard Clean Architecture abstractions making it incredibly easy to extend. It supports graceful degradation, multi-LLM routing (OpenAI, Anthropic, Google Gemini), and relies on a local SQLite or Postgres database to keep track of its asynchronous review jobs and feedback metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Try It Out
&lt;/h3&gt;

&lt;p&gt;If your team uses GitLab and you're looking for a smarter, less noisy AI reviewer—or if you're just interested in Go-based AI Agents—I'd love for you to check it out.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/antlss/gitlab-review-agent" rel="noopener noreferrer"&gt;antlss/gitlab-review-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm actively looking for feedback, feature requests, and early contributors! Let me know in the comments: What is the most annoying comment an AI reviewer has ever left on your PR?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>go</category>
      <category>gitlab</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
