Document Smarter, Not Harder: Automate Go API Docs
Let’s be honest with ourselves, most software engineers, including myself, don’t like to document. It is boring, monotonous and why not say it, almost the worst part of the whole software development process.
Documentation is like sex; when it’s good, it’s very, very good, and when it’s bad, it’s better than nothing.”
— Dick Brandon
Hi! I’m Javi 👋 A software engineer with +6 years of experience in backend development. During my experience as a developer I have seen and made many mistakes when documenting. That is why I would like to share this guide to help you avoid making them.
Here are the top ones:
- Updating an endpoint and not reflecting the changes in the documentation.
- Not documenting because “I already sent a slack to the frontend team on how the API works”.
- Incomplete documentation, e.g. not providing a schema of a post request or an example payload.
- Updating documentation out of time with the release.
- Broken links or links pointing to obsolete content.
How to automate the process of documenting APIs step-by-step
The advantage of the documentation first process is that we use the documentation as the basis of our code, i.e. the Open API yaml itself is go code for us.
Let’s make a simple but complete example to see the potential of this solution.
Note: to get to the point, we’ll skip the open api codegen configuration. You’ll find a complete example of it in our Github repository.
Step 1- Define the Open API specification
For this example we will create an endpoint that returns an entity by its identifier.
Endpoint specification:
- Endpoint: GET /goats/{id}.
- Query param ID is of type UUID.
- The returned object (if found) must be a custom Goat object with 200 OK status.
- If the id is not found, return a 404 Not Found with an error message.
spec.yaml
paths:
/goats/{id}:
get:
summary: Get a goat by id
description: This endpoint returns a goat by id
tags:
- goats
operationId: getGoatByID
parameters:
- $ref: '#/components/parameters/id'
responses:
'200':
description: All good
content:
application/json:
schema:
$ref: '#/components/schemas/Goat'
'404':
$ref: '#/components/responses/404'
components:
schemas:
Goat:
type: object
description: Goat representation
required:
- id
- name
- age
properties:
id:
$ref: '#/components/schemas/UUID'
name:
type: string
example: 'Lovely Goat'
age:
type: integer
format: uint
example: 3
UUID:
type: string
x-go-type: uuid.UUID
x-go-type-import:
name: uuid
path: github.com/google/uuid
example: 3267b999-a3bc-482e-aabd-cb2b2b618cb5
ErrorMessage:
type: object
required:
- message
properties:
message:
type: string
example: 'Some error message'
parameters:
id:
name: id
in: path
required: true
description: |
UUID parameter, e.g: 8edd8112-c415-11ed-b036-debe37e1cbd6
schema:
type: string
x-go-type: uuid.UUID
x-go-type-import:
name: uuid
path: github.com/google/uuid
responses:
'404':
description: 'Not found'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorMessage'
Step 2 - Generate go code from the specification
Once the specification is ready, we can generate the go code from it. In order to achieve that easily we have made a Makefile that you can find in the Github example repository. The Makefile command uses a popular library called oapi-codegen from deepmap that transforms the Open API specification into go code.
make api
api.gen.go
// ErrorMessage defines model for ErrorMessage.
type ErrorMessage struct {
Message string `json:"message"`
}
// Goat Goat representation
type Goat struct {
Age uint `json:"age"`
Id UUID `json:"id"`
Name string `json:"name"`
}
// UUID defines model for UUID.
type UUID = uuid.UUID
// Id defines model for id.
type Id = uuid.UUID
// N404 defines model for 404.
type N404 = ErrorMessage
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Get a goat by id
// (GET /goats/{id})
GetGoatByID(w http.ResponseWriter, r *http.Request, id Id)
}
type N404JSONResponse ErrorMessage
type GetGoatByIDRequestObject struct {
Id Id `json:"id"`
}
type GetGoatByID200JSONResponse Goat
type GetGoatByID404JSONResponse struct{ N404JSONResponse }
// GetGoatByID operation middleware
func (siw *ServerInterfaceWrapper) GetGoatByID(w http.ResponseWriter, r *http.Request) {
...
}
Step 3 - Implement the endpoint logic
With the generated code ready, the only thing left to do is to create a function in our service that implements the interface of the defined endpoint.
As you can see in the snippet below, all objects and function signature of the GetGoatByIDxxx form are automatically generated through the specification with the open-api codegen tool.
package api
import (
"context"
"github.com/google/uuid"
)
var defaultGoat = Goat{
Id: uuid.MustParse("3267b999-a3bc-482e-aabd-cb2b2b618cb5"),
Name: "lovely goat",
Age: 2,
}
func (s *Server) GetGoatByID(_ context.Context, req GetGoatByIDRequestObject) (GetGoatByIDResponseObject, error) {
if req.Id == defaultGoat.Id {
return GetGoatByID200JSONResponse{
Id: defaultGoat.Id,
Name: defaultGoat.Name,
Age: defaultGoat.Age,
}, nil
}
return GetGoatByID404JSONResponse{N404JSONResponse{Message: "Goat not found"}}, nil
}
Step 4 - Make documentation available
Last but not least, we need to make available the documentation to the rest of the developers. In order to achieve this, we will use one of the best and highly customisable specification viewers called RapiDoc.
spec.html
<!doctype html>
<html>
<head>
<title>Open api codegen - Demo</title>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/static/docs/api/api.yaml"
theme = "light"
allow-spec-url-load = false
allow-spec-file-load = false
show-header = false
render-style = "read"
show-method-in-nav-bar = "as-colored-text"
navigation-item-text = path
allow-authentication = true
sort-endpoints-by="summary"
>
<!-- <img slot="nav-logo" src="{logo}" style="width:50px; height:50px"/>-->
</rapi-doc>
</body>
</html>
With the spec.html ready it’s time to serve the docs from the go http server:
static.go
package api
import (
"context"
"net/http"
"os"
"github.com/go-chi/chi/v5"
)
// GetDocumentation this method will be overridden in the main function
func (s *Server) GetDocumentation(_ context.Context, _ GetDocumentationRequestObject) (GetDocumentationResponseObject, error) {
return nil, nil
}
// RegisterStatic add method to the mux that are not documented in the API.
func RegisterStatic(mux *chi.Mux) {
mux.Get("/docs", documentation)
mux.Get("/static/docs/api/api.yaml", swagger)
}
func documentation(w http.ResponseWriter, _ *http.Request) {
writeFile("api/spec.html", "text/html; charset=UTF-8", w)
}
func swagger(w http.ResponseWriter, _ *http.Request) {
writeFile("api/api.yaml", "text/html; charset=UTF-8", w)
}
func writeFile(path string, mimeType string, w http.ResponseWriter) {
f, err := os.ReadFile(path)
if err != nil {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("not found"))
}
w.Header().Set("Content-Type", mimeType)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(f)
}
Now you and your team can consult your always up to date documentation:
Pros and cons of this solution:
No solution is perfect, but in my experience and after having worked for more than a year with this process, I can say that the quality of documentation has increased exponentially and the number of complaints due to outdated or incomplete documentation has been drastically reduced.
Pros:
- Streamlines the documentation process.
- Compatible with all major HTTP Routers (Echo, Chi, Gin, gorilla/mux, Fiber, and Iris).
- Saves development time with the autogenerated boilerplate.
- Avoids inconsistencies between the code and the documentation.
- Define common json tags as omit-empty or json-ignore and required or non required fields in the yaml specification itself.
- Highly customisable, for example, adding endpoint securisation through the Open API specification (with its corresponding middleware).
Cons:
- Small learning curve for open api with go syntax for some custom data types.
- Adding a tiny overhead with the auto-generated code.
Thank you for following along thus far - I hope you’ve found it valuable. Keep an eye out for upcoming content!
Now, it’s your turn: what documentation process do you use? Have you encountered major challenges, and if so, how did you overcome them?
Resources: