net/go.book
All Parts Marketing

API Documentation and OpenAPI/Swagger

A REST endpoint is only as useful as its documentation. OpenAPI (formerly known as Swagger) is the industry-standard, machine-readable format for describing an HTTP API — its paths, request/response shapes, authentication, and error codes — in a way that both humans and tools (client generators, testing tools, API gateways) can consume.


What OpenAPI Actually Is

An OpenAPI document is a YAML or JSON file describing your API's surface. At a high level, it has three main parts:

  • paths: every route and HTTP method your API exposes, with parameters, request bodies, and possible responses.
  • components/schemas: reusable data shapes (a Product, a User, an Error) referenced from multiple paths so you define each shape once.
  • info, servers, security: metadata — API title and version, base URLs, and how authentication works (API key, bearer token, OAuth2).

A minimal excerpt:

openapi: 3.0.3
info:
  title: Task API
  version: "1.0.0"
paths:
  /tasks/{id}:
    get:
      summary: Get a task by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: The requested task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Task not found
components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: integer
        text:
          type: string
        done:
          type: boolean

Once this document exists, Swagger UI (or an equivalent renderer) turns it into an interactive page where anyone can browse endpoints and try requests directly from the browser — without writing a single line of frontend code.


Spec-First vs Code-First

There are two ways to arrive at that YAML file:

  • Spec-first: write the OpenAPI document by hand (or generate it from a design tool) before writing any code, then implement handlers that match it. This keeps the contract stable and lets frontend and backend teams work in parallel from an agreed shape.
  • Code-first: write Go handlers first, and annotate them with comments that a tool scans to generate the OpenAPI document automatically. swaggo/swag is the standard tool for this in the Go ecosystem.

Neither approach is universally "correct" — spec-first suits teams that value the contract as the source of truth; code-first suits smaller teams that want documentation to stay physically next to the code it describes.


Code-First with swaggo/swag

go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/http-swagger

Annotate a handler with structured comments directly above its function:

// GetTask godoc
// @Summary      Get a task by ID
// @Description  Returns a single task
// @Tags         tasks
// @Produce      json
// @Param        id   path      int  true  "Task ID"
// @Success      200  {object}  Task
// @Failure      404  {object}  ErrorResponse
// @Router       /tasks/{id} [get]
func (a *API) getTask(w http.ResponseWriter, r *http.Request) {
	// ... handler body as in chapter 4 ...
}

Running swag init scans the codebase for these @-prefixed annotations and generates a docs/ package containing the OpenAPI JSON/YAML plus a Go file the router can serve. Wiring the generated docs into your net/http mux:

import httpSwagger "github.com/swaggo/http-swagger"

mux.Handle("/swagger/", httpSwagger.WrapHandler)

Visiting /swagger/index.html now shows a live, browsable, testable version of the API — regenerated every time you rerun swag init after changing annotations.


Real-World Example: What Swagger UI Shows

Given the annotation above, Swagger UI renders an entry like:

GET /tasks/{id}
  Parameters: id (integer, required, in path)
  Responses:
    200 - Task { id, text, done }
    404 - ErrorResponse { error }
  [Try it out]

Clicking "Try it out," entering an ID, and executing sends a real GET /tasks/3 to your running server and displays the actual response — useful for manual QA and for onboarding a new engineer who needs to understand the API's shape quickly.


Validating Requests Against the Spec

Beyond display, an OpenAPI document is machine-checkable: request validation middleware can reject a payload that doesn't match the schema before your handler ever sees it, using a library like github.com/getkin/kin-openapi to load the spec and check incoming requests against it:

import (
	"log"
	"net/http"

	"github.com/getkin/kin-openapi/openapi3"
	"github.com/getkin/kin-openapi/openapi3filter"
	"github.com/getkin/kin-openapi/routers"
	legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
)

var apiRouter routers.Router

func init() {
	loader := openapi3.NewLoader()
	doc, err := loader.LoadFromFile("openapi.yaml")
	if err != nil {
		log.Fatal(err)
	}
	apiRouter, err = legacyrouter.NewRouter(doc)
	if err != nil {
		log.Fatal(err)
	}
}

func validationMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		route, pathParams, err := apiRouter.FindRoute(r)
		if err == nil {
			reqValidationInput := &openapi3filter.RequestValidationInput{
				Request: r, PathParams: pathParams, Route: route,
			}
			err := openapi3filter.ValidateRequest(
				r.Context(), reqValidationInput,
			)
			if err != nil {
				http.Error(
					w, "request does not match API schema",
					http.StatusBadRequest,
				)
				return
			}
		}
		next.ServeHTTP(w, r)
	})
}

This turns the spec from documentation-only into an enforced contract — a client sending an extra required field or the wrong type gets a 400 immediately, with no handler code needed to check for it.


Keeping Documentation Honest

Documentation that drifts from the actual behavior of the API is worse than no documentation — it actively misleads. A few practices keep it trustworthy:

  • Generate from code or contract, don't hand-maintain a separate document. Whichever direction you chose (spec-first or code-first), let one artifact be the single source of truth and derive the other from it.
  • Validate the spec in CI. Tools like swag fmt (for annotation formatting) or an OpenAPI linter can catch a broken or stale spec before it merges.
  • Document error responses, not just the happy path. A client integrating against your API needs to know what a 400 or 404 body looks like just as much as what a 200 looks like.
  • Version the spec alongside the API itself — when you introduce /v2/tasks, publish a v2 OpenAPI document rather than silently mutating the v1 one (versioning is covered in depth in a later chapter).

Documentation that isn't generated from the same source as your handlers will eventually lie — treat drift between the two as a bug, not a documentation nitpick.


Frequently Asked Questions

I ran swag init but /swagger/index.html still shows the old response shape — what did I miss? The generated docs/ package only reflects the state of your @-prefixed annotations as of the last time swag init ran, so any handler comment you've edited since then hasn't been picked up yet. Rerun swag init after every annotation change, and treat forgetting to do so as exactly the kind of spec-drift the chapter warns about.

Should I write the OpenAPI YAML by hand or use swaggo/swag to generate it from comments? It depends on which team is the source of truth. Spec-first keeps the contract stable and lets frontend and backend build in parallel from an agreed shape, while code-first with swaggo/swag keeps the documentation physically next to the handler it describes, which is usually the more practical default for a smaller team.

Does Swagger UI's "Try it out" button send a fake request, or does it actually hit my server? It's a real request — clicking "Try it out" and executing sends an honest GET /tasks/3 (or whatever the entry describes) to your running server and shows the actual response body, not a canned example. That's what makes it useful for manual QA and for a new engineer exploring the API's real behavior.

Once I have an OpenAPI document, is validating requests against it optional, or does it happen automatically? It's entirely opt-in — the spec is just documentation until you wire up something like github.com/getkin/kin-openapi as middleware to actually check incoming requests against it. Without that middleware, a client can send an extra field or the wrong type and your handler will still see it; the middleware shown in this chapter is what turns the spec into an enforced 400-on-mismatch contract.

If I add a /v2/tasks endpoint, do I edit the existing OpenAPI document in place? No — publish a separate v2 OpenAPI document rather than mutating the v1 one, the same way the API itself should expose /v2/tasks as a new route rather than silently changing /tasks's behavior. Versioning the spec alongside the API keeps clients still on v1 from being blindsided by a document that no longer matches what they're calling.

With the API surface now discoverable and documented, the next chapter turns to proving it actually behaves as documented: testing and mocking APIs in Go.