How We Documented 95 API Endpoints with OpenAPI and Scalar

Published: (February 23, 2026 at 06:06 PM EST)
6 min read
Source: Dev.to
> **Source:** [Dev.to](https://dev.to/alexneamtu/how-we-documented-95-api-endpoints-with-openapi-and-scalar-474i)

[![Alex Neamtu](https://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F353763%2Fad2f8461-24ae-4014-8af9-f848d9cf4c5b.jpeg)](https://dev.to/alexneamtu)

SendRec has grown to **95 API endpoints** covering authentication, video management, playlists, folders, tags, billing, webhooks, and public watch pages. We needed documentation that stayed in sync with the code and didn’t require a separate build step. Here’s how we set it up.

---

The approach: embedded YAML + Scalar

We wanted three things:

  1. A single YAML file we could lint and test.
  2. Interactive docs that render in the browser.
  3. Zero infrastructure beyond the Go binary itself.

Embed the OpenAPI spec

The OpenAPI spec lives at internal/docs/openapi.yaml, and Go’s //go:embed directive bakes it into the binary at compile time:

package docs

import (
    _ "embed"
    "net/http"
)

//go:embed openapi.yaml
var specYAML []byte

func HandleSpec(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/yaml")
    _, _ = w.Write(specYAML)
}

Serve the interactive UI

We serve a minimal HTML page that loads Scalar from a CDN:

func HandleDocs(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    _, _ = w.Write([]byte(docsHTML))
}

const docsHTML = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>SendRec API Reference</title>
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</head>
<body>
  <scalar-api-reference
    spec-url="/openapi.yaml"
    layout="sidebar"
    hide-hostname
  ></scalar-api-reference>
</body>
</html>`

Two routes, no build step, no static‑file server. The docs page is gated behind an API_DOCS_ENABLED=true environment variable so self‑hosters can decide whether to expose it.

Content Security Policy

Scalar loads JavaScript and CSS from cdn.jsdelivr.net, so the docs handler sets a targeted CSP header:

w.Header().Set("Content-Security-Policy",
    "default-src 'self'; " +
        "script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; " +
        "style-src  'self' https://cdn.jsdelivr.net 'unsafe-inline'; " +
        "font-src   'self' https://cdn.jsdelivr.net data:; " +
        "img-src    'self' data:; " +
        "connect-src 'self'; " +
        "frame-ancestors 'self';")

This keeps the security headers tight while allowing Scalar to function. The rest of the application uses a stricter default policy.

Structuring 4,200 Lines of YAML

When dealing with 95 endpoints, a clear organization is essential. We group related endpoints using tags and keep reusable definitions in components/schemas.

Tags

tags:
  - name: Authentication
    description: User registration, login, and token management
  - name: Videos
    description: Video CRUD, sharing, and management
  - name: Playlists
    description: Playlist management and sharing
  - name: Folders
    description: Video folder management
  - name: Tags
    description: Video tag management
  - name: Settings
    description: User account settings and notification preferences
  - name: Watch
    description: Public video watching and interaction
  - name: Billing
    description: Subscription and payment management

Endpoint Structure

  • tag – assigns the endpoint to one of the groups above.
  • operationId – a unique identifier for the operation (used by code generators).
  • summary – a brief, human‑readable description of what the endpoint does.

Reusable Schemas

All request bodies, response objects, and error formats are defined under components/schemas.
We maintain roughly 75 schema definitions, which keeps the specification DRY and easier to maintain.

YAML Gotchas

A subtle bug broke the docs page entirely. Scalar showed “Document ‘api‑1’ could not be loaded” with no further details. The culprit was a description field containing back‑tick‑wrapped JSON:

# Broken — YAML interprets `: ` inside the value as a mapping
description: Email not verified. Error body contains `{"error": "email_not_verified"}`.

Fix

Wrap the entire description in single quotes so the inner JSON is treated as a plain string:

# Fixed
description: 'Email not verified. Error body contains {"error": "email_not_verified"}.'

The : inside "error": "email_not_verified" caused the YAML parser to treat everything after the colon as a nested mapping value. This is easy to miss when writing YAML by hand, especially in inline descriptions.

Testing the Specification

The following test ensures that every critical endpoint is present in the embedded OpenAPI specification. It scans the generated specYAML and verifies that each expected path appears in the document. This guard catches the common mistake of adding a new route in server.go without updating the spec. The test runs automatically in CI alongside the rest of the suite.

func TestSpecContainsAllEndpoints(t *testing.T) {
    // Load the generated OpenAPI spec as a string.
    spec := string(specYAML)

    // List of endpoints that must be documented.
    endpoints := []string{
        "/api/health",
        "/api/auth/register",
        "/api/auth/login",
        "/api/videos",
        "/api/videos/limits",
        "/api/watch/{shareToken}",
        "/api/playlists",
        "/api/folders",
        // ... all 95 endpoints
    }

    // Verify each endpoint is present in the spec.
    for _, ep := range endpoints {
        if !strings.Contains(spec, ep) {
            t.Errorf("spec missing endpoint: %s", ep)
        }
    }
}

Note: Keep the endpoints slice up‑to‑date whenever new routes are added to the server. This test will fail if any endpoint is omitted from the OpenAPI documentation.

What the spec covers

The full specification documents the following areas:

AreaDescription
AuthenticationRegister, login, token refresh, logout, password reset, email confirmation
VideosCRUD operations, upload, trim, download, thumbnails, transcription, AI‑generated summaries, password protection, comments, analytics, email gates, CTAs
PlaylistsCreate, list, update, delete, add/remove/reorder videos, shared watch pages
Folders & TagsOrganization, tagging, bulk operations
SettingsUser account settings, notification preferences
WatchPublic video watching, interaction, share tokens
BillingSubscription plans, payment processing, invoices

All of this lives in a single openapi.yaml file, embedded in the binary, served via the two tiny handlers above, and kept in sync with the codebase through automated tests.

Features

  • Videos – organize with folders and color‑coded tags
  • Batch operations – bulk delete, bulk move to folder, bulk tag
  • Settings – notification preferences, Slack webhooks, custom webhooks, branding, API keys
  • Billing – checkout, subscription status, cancellation
  • Watch pages – public video viewing, password verification, comments, milestones, oEmbed

Each endpoint includes:

  • Request/response schemas
  • Required fields
  • Authentication requirements
  • HTTP status codes

Why not code generation?

We considered generating the spec from Go struct tags or handler annotations, but decided against it. Hand‑written YAML gives us full control over:

  • Descriptions
  • Examples
  • Grouping

The spec is a documentation artifact, not a code contract — we’d rather have clear docs than auto‑generated ones that merely mirror the code without adding context.

Trade‑off: we must keep the spec in sync manually.

  • The endpoint test catches missing paths.
  • Code review catches the rest.

Try it

The live API docs are at app.sendrec.eu/api/docs.
SendRec is open source — the full spec and serving code are in the GitHub repo.

0 views
Back to Blog

Related posts

Read more »