<?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: Galih Putro Aji</title>
    <description>The latest articles on DEV Community by Galih Putro Aji (@galih_putroaji).</description>
    <link>https://dev.to/galih_putroaji</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4113013%2F955ae32d-6a2a-47ac-a9a1-e276c6158818.png</url>
      <title>DEV Community: Galih Putro Aji</title>
      <link>https://dev.to/galih_putroaji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/galih_putroaji"/>
    <language>en</language>
    <item>
      <title>Why We Dumped Swagger UI for Scalar in Our Go Backend</title>
      <dc:creator>Galih Putro Aji</dc:creator>
      <pubDate>Mon, 07 Sep 2026 02:33:17 +0000</pubDate>
      <link>https://dev.to/galih_putroaji/why-we-dumped-swagger-ui-for-scalar-in-our-go-backend-1bfp</link>
      <guid>https://dev.to/galih_putroaji/why-we-dumped-swagger-ui-for-scalar-in-our-go-backend-1bfp</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5hrw5bef27e6iw8fy2a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5hrw5bef27e6iw8fy2a.png" alt="Why We Dumped Swagger UI for Scalar in Our Go Backend" width="799" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Why We Dumped Swagger UI for Scalar in Our Go Backend&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few weeks ago, at 4:45 PM on a Friday (because of course it was Friday), our frontend engineer dropped a message on Slack:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hey, why is the API playground on staging trying to send requests to localhost:8080? Did someone leave their dev server running on a laptop somewhere?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Silence in the channel…&lt;br&gt;&lt;br&gt;
That single innocent question exposed a dirty secret: our API documentation was broken, outdated, and living in an alternate universe.&lt;/p&gt;

&lt;p&gt;Like almost every Go developer over the last five years, we had faithfully followed the holy Go ritual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write magic comments above handler functions like a medieval monk copying manuscripts (// &lt;a class="mentioned-user" href="https://dev.to/param"&gt;@param&lt;/a&gt; user_id path string true "User ID").&lt;/li&gt;
&lt;li&gt;Run swag init and pray we didn't miss a comma.&lt;/li&gt;
&lt;li&gt;Mount Swagger UI on /swagger/*.&lt;/li&gt;
&lt;li&gt;Congratulate ourselves on being “documentation champions.”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It worked initially. But as our backend grew, that ritual turned into a comedy of errors.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Three Pain Points That Drove Us Crazy
&lt;/h3&gt;

&lt;p&gt;Before we threw Swagger UI out the window, we were dealing with three daily frustrations:&lt;/p&gt;
&lt;h4&gt;
  
  
  1. The “Doc Drift” Trap
&lt;/h4&gt;

&lt;p&gt;Your annotations live as comments above HTTP handlers. You refactor a Go struct inside a domain package, but forget to update the 15 lines of magic comments floating above your handler function.&lt;/p&gt;

&lt;p&gt;Result? Your docs claim the endpoint returns a plain string, but your Go code returns a nested JSON object. Your frontend team spends two hours debugging phantom errors while you sip coffee completely unaware.&lt;/p&gt;
&lt;h4&gt;
  
  
  2. The 2015-Era UI Experience
&lt;/h4&gt;

&lt;p&gt;Let’s be honest: Swagger UI looks like it was designed when Internet Explorer 8 was still a thing. Testing JWT-authenticated endpoints, reading nested JSON error schemas, and searching across 30+ endpoints felt like navigating a government tax portal.&lt;/p&gt;
&lt;h4&gt;
  
  
  3. The Hardcoded Host Nightmare
&lt;/h4&gt;

&lt;p&gt;Our generated spec had host: "localhost:8080" baked inside it. When QA opened the docs on staging (&lt;a href="https://staging-api.example.com/docs" rel="noopener noreferrer"&gt;https://staging-api.example.com/docs&lt;/a&gt;) to test a release, every click on "Try it out" silently fired HTTP calls to localhost:8080.&lt;/p&gt;

&lt;p&gt;Cue 30 minutes of classic “It works on my machine!” debates.&lt;/p&gt;
&lt;h3&gt;
  
  
  Enter Scalar: Single Source of Truth + Zero Build Overhead
&lt;/h3&gt;

&lt;p&gt;We decided we didn’t want another 50MB npm build step in our CI pipeline, nor did we want complex static asset embedding in our Go binary. We wanted something dead simple, ridiculously sleek, and impossible to break.&lt;/p&gt;

&lt;p&gt;That’s when we dumped Swagger UI and switched to  &lt;strong&gt;Scalar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of scattering annotations across 40 Go files, we made a radical move: a single api/openapi/openapi.yaml file as the &lt;strong&gt;one true source of truth&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go-backend/
├── api/
│ └── openapi/
│ └── openapi.yaml &amp;lt;-- One True Source of Truth
├── internal/
│ └── delivery/
│ └── http/
│ ├── handler/
│ │ ├── docs.go &amp;lt;-- Serves Scalar HTML &amp;amp; openapi.yaml
│ │ └── docs_test.go &amp;lt;-- Contract &amp;amp; security tests
│ └── router.go &amp;lt;-- Environment guardrails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scalar runs as a modern, zero-build web component (@scalar/api-reference) straight from a CDN. No Node.js, no Webpack, no build step.&lt;/p&gt;

&lt;p&gt;All our Go backend needs to do is serve a 15-line HTML shell and the raw YAML file in internal/delivery/http/handler/docs.go:&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;package&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;

&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DocsHandler&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;openAPIPath&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewDocsHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;openAPIPath&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DocsHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;openAPIPath&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;openAPIPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api/openapi/openapi.yaml"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;DocsHandler&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;openAPIPath&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;openAPIPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// RenderUI serves the Scalar API reference HTML shell&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;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DocsHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RenderUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&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="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;API Reference&amp;lt;/title&amp;gt;
    &amp;lt;meta charset="utf-8" /&amp;gt;
    &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1" /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;!-- Scalar CDN script integration --&amp;gt;
    &amp;lt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`script id="api-reference" data-url="/docs/openapi.yaml"&amp;gt;&amp;lt;/`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`script&amp;gt;
    &amp;lt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`script src="&amp;lt;https://cdn.jsdelivr.net/npm/@scalar/api-reference&amp;gt;"&amp;gt;&amp;lt;/`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;`script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`&lt;/span&gt;
 &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeaderContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIMETextHTMLCharsetUTF8&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ServeSpec serves the raw OpenAPI YAML file&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;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DocsHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ServeSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFile&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;openAPIPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"OpenAPI specification not found"&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeaderContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/yaml"&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice data-url="/docs/openapi.yaml": the browser automatically fetches the contract relative to whatever host served the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Secret Sauce: Relative Routing (url: /api)
&lt;/h3&gt;

&lt;p&gt;The real magic that cured our staging headaches was replacing full domain URLs in openapi.yaml with a single relative path:&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;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.0.3&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Go Service API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api&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;Same-origin API server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By defining url: /api, Scalar's HTTP client attaches test requests directly to whichever domain is currently in the address bar:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w3m9bv8ir1npmwkrdf8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w3m9bv8ir1npmwkrdf8.png" alt="image2" width="799" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No environment variables, no YAML templating hackery, and zero CORS errors on staging. It just works because the browser already knows where it is!&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping Production Locked Down (404 for Unexpected Visitors)
&lt;/h3&gt;

&lt;p&gt;As much as we love interactive API playgrounds, we don’t want bored internet scanners getting a free interactive map of our internal endpoints in production.&lt;/p&gt;

&lt;p&gt;In internal/delivery/http/router.go, we locked the /docs route behind an environment check:&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="n"&gt;RegisterRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&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="n"&gt;docsHandler&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DocsHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="c"&gt;// ... register auth, user, referee routes ...&lt;/span&gt;

 &lt;span class="c"&gt;// Security Guardrail: Mount /docs only in dev, test, and staging.&lt;/span&gt;
 &lt;span class="c"&gt;// In production, /docs returns a polite 404 Not Found.&lt;/span&gt;
 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isProduction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;docsGroup&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/docs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;docsGroup&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;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docsHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderUI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;docsGroup&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.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docsHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeSpec&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isProduction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&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;env&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an automated vulnerability scanner hits GET /docs on production, Fiber responds with a standard 404 Not Found. Move along, nothing to see here!&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying It All in 5 Milliseconds
&lt;/h3&gt;

&lt;p&gt;To make sure nobody accidentally breaks our routing or leaks docs to production during a late-night refactor, we wrote a quick unit test (internal/delivery/http/handler/docs_test.go):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;func TestDocsOpenAPISpec_UsesRelativeAPIServer(t *testing.T) {
 app := fiber.New()
 docsHandler := handler.NewDocsHandler("../../../../api/openapi/openapi.yaml")
 app.Get("/docs/openapi.yaml", docsHandler.ServeSpec)

 req := httptest.NewRequest(http.MethodGet, "/docs/openapi.yaml", nil)
 resp, err := app.Test(req)
 if err != nil || resp.StatusCode != http.StatusOK {
  t.Fatalf("expected 200 OK, got %d", resp.StatusCode)
 }

 buf := make([]byte, 1024)
 n, _ := resp.Body.Read(buf)
 content := string(buf[:n])

 if !strings.Contains(content, "url: /api") {
  t.Errorf("expected openapi spec to define relative server 'url: /api', got:\n%s", content)
 }
}

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; ./internal/delivery/http/handler &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-run&lt;/span&gt; &lt;span class="s2"&gt;"TestDocs"&lt;/span&gt;
&lt;span class="go"&gt;=== RUN TestDocsRoutes_NonProduction_Serves200
--- PASS: TestDocsRoutes_NonProduction_Serves200 (0.00s)
=== RUN TestDocsOpenAPISpec_UsesRelativeAPIServer
--- PASS: TestDocsOpenAPISpec_UsesRelativeAPIServer (0.00s)
PASS
ok go-backend/internal/delivery/http/handler    0.004s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Moral of the Story
&lt;/h3&gt;

&lt;p&gt;Switching from Swagger UI to Scalar delivered three huge wins for our team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Doc Drift&lt;/strong&gt; : PRs review openapi.yaml directly—no more hidden annotation bugs inside Go handler comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Build Overhead&lt;/strong&gt; : Scalar runs in the browser via CDN web component. No npm dependencies or binary bloat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Happier Frontend Devs&lt;/strong&gt; : Staging playgrounds actually work, and the UI feels like a modern developer tool built in this decade.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team is still wrestling with broken Swagger playgrounds or 2015-era UI, dumping Swagger for Scalar is easily one of the highest-ROI Developer Experience (DX) wins you can score in an afternoon.&lt;/p&gt;

</description>
      <category>fiber</category>
      <category>softwaredevelopment</category>
      <category>api</category>
      <category>devrel</category>
    </item>
  </channel>
</rss>
