Why Define an API Standard?

In the earlier parts of this series, our REST API's definition was little more than a list of method/path pairs with comments. That works for a small internal project, but it doesn't scale well once external clients need to integrate, or when non-engineers need to understand the contract.

POST   /task/              :  create a task, returns ID
GET    /task/<taskid>      :  returns a single task by ID
GET    /task/              :  returns all tasks
DELETE /task/<taskid>      :  delete a task by ID
GET    /tag/<tagname>      :  returns list of tasks with this tag
GET    /due/<yy>/<mm>/<dd> :  returns list of tasks due by this date

A formal, machine-readable API description changes that. It becomes a precise contract between server and client, immediately familiar to anyone who has worked with REST APIs, and it unlocks automation — from documentation generation to code scaffolding to cloud-provider integrations.

From Swagger to OpenAPI

The tooling ecosystem here has a somewhat confusing history. What started as Swagger in 2011 — primarily an IDL for REST APIs — evolved into the OpenAPI specification. Version 2.0 arrived in 2014, and by 2016, a coalition of industry players had standardized the format as OpenAPI 3.0.

Swagger logo

A useful rule of thumb: OpenAPI refers to the current specification itself, while Swagger generally labels the tooling built around it (though you will still hear "Swagger spec" for versions predating 3.0). The official hub is https://swagger.io, supported by Smart Bear Software.

Modeling Our Task Service

To see this in practice, we rewrote the familiar task server from scratch, this time starting with an OpenAPI definition typed into the Swagger Editor. The resulting YAML file describes every endpoint. For instance, the spec for GET /task/ — which returns all tasks — allows an arbitrary set of paths, each with its methods, parameters, responses, and JSON schemas:

/task:
  get:
    summary: Returns a list of all tasks
    responses:
      '200':
        description: A JSON array of task IDs
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/Task'

Here, components/schemas/Task references the model's definition elsewhere in the spec:

components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: integer
        text:
          type: string
        tags:
          type: array
          items:
            type: string
        due:
          type: string
          format: date-time

Notably, this schema defines types for data fields. That detail opens the door for auto-generated validation, even if, as we'll see, the tooling doesn't always capitalize on it.

Writing the spec already pays off: it yields clean, navigable documentation. The actual docs are clickable and expandable, offering a clear breakdown of request parameters, responses, and their JSON schemas.

Swagger-generated documentation image for our REST API

If the API is publicly hosted, clients can also interact with it directly from the Swagger Editor, staging auto-generated, schema-aware requests instead of hand-writing curl commands. For non-engineers on the product or design side, this is a practical way to experiment without writing scripts.

Auto-Generating a Go Server

Documentation is nice, but the bigger promise is code generation. Following the official instructions for swagger-codegen, we generated a Go server skeleton and then filled in the handlers to match our task logic. The generated scaffold uses gorilla/mux for routing, much like our earlier router-based approach, and creates placeholder handlers in a file named api_default.go.

func TaskIdDelete(w http.ResponseWriter, r *http.Request) {
  id, err := strconv.Atoi(mux.Vars(r)["id"])
  if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }
  err = store.DeleteTask(id)
  if err != nil {
    http.Error(w, err.Error(), http.StatusNotFound)
  }
}

The experience, however, revealed several limitations:

  • Package naming and imports were broken out of the box; fitting the generated code to Go modules required restructuring.
  • Some generated files didn't comply with gofmt formatting.
  • The server relies on globals. Earlier in the series, we used server structs whose registered methods shared data as fields; here, handlers are top-level functions.
  • Even though the OpenAPI spec explicitly typed certain path parameters as integers (e.g., day, month, year in the due/ path), the generated server contained zero validation for them. The code didn't even use gorilla/mux's support for regexp in path parameters — so manual parameter validation was needed again.

On balance, the time saved by auto-generating the server is modest, since a lot of the code had to be rewritten anyway. Worse, this benefit applies only once: if the OpenAPI definition changes later, regenerating the server isn't practical because the generated scaffold and your custom code have already diverged.

Alternative Code Generators

Since OpenAPI specs follow a documented YAML format, plenty of tools compete with the official Swagger generator. For Go, one prominent option is go-swagger, which is blunt about its advantages:

How is this different from go generator in swagger-codegen?

tl;dr The main difference at this moment is that this one actually works...

The swagger-codegen project only generates a workable go client and even there it will only support flat models. Further, the go server generated by swagger-codegen is mostly a stub.

Truthfully, the go-swagger-generated server is far more feature-complete. But that comes with a cost: it locks you into a specific framework. The output is heavily dependent on the packages in the github.com/go-openapi organization and extensively uses them for runtime setup, even adding its own custom flag parsing. If you prefer a particular router or design, the opinionated output isn't easy to adapt. Also, go-swagger only supports Swagger 2.0, not the newer OpenAPI 3.0, requiring a spec conversion step (via an online tool) to get the current format in.

A more promising route, added after reader feedback, is oapi-codegen. Its output was significantly cleaner, plainly separating generated boilerplate from custom code, and it accepts OpenAPI v3 specs. The main complaint is that it pulls in a third-party dependency to implement request parameter binding — something that would be better handled as a configurable inline solution rather than a forced dependency.

Generating Specs from Existing Code

What if your server is already built and you just want an OpenAPI spec for documentation or governance? Tools like swaggo/swag read special comment annotations in your code and emit a spec — though again, this approach is limited to spec 2.0. That spec can then feed into Swagger's documentation tooling.

For projects that already have a deliberate server architecture and don't want to lock into a new framework, this annotation-driven route is often the most practical path to having a formal API description.

Choosing Your OpenAPI Comfort Level

Imagine two competing REST APIs. One ships with an ad-hoc text file and a few curl examples. The other provides a proper OpenAPI spec, complete with standard documentation and an in-browser try-out console. Given equal functionality, it’s hard to argue against opening the spec version first. The value of OpenAPI as a standardized contract between server and client is clear.

What’s less clear is how far to push the Swagger toolchain. Using OpenAPI for documentation and API description is an easy win. Code generation is a different matter. Auto-generated servers can be great for quick prototyping and experimentation, but they’re not a solid foundation for production code where you want firm control over structure and dependencies.

A middle ground sits with tools like swaggo/swag. You keep hand-written server code in your preferred framework and structure, then annotate it with magic comments that describe the REST API. The tool derives the OpenAPI spec from those comments, which you can then use for documentation or other downstream tasks. This approach keeps the source of truth — the annotations — physically tied to the code that implements the endpoints, which is a sound engineering practice.

[1]Recall the description of the manual.sh script from Part 1. This script contains a collection of curl commands to interact with our server. It's clear that such commands can be auto-generated from a standardized description of the REST API, saving lots of work.
[2]Of course, my inclination could be different if my job was to churn out a new REST server every Thursday. Always keep Benefits of dependencies in software projects as a function of effort in mind.