One of the things that shocked me the most when I changed from C# to Golang is that developers are reluctant of adding libraries to solve problems unless it is strictly needed. This sentiment, at first annoying, turned out to be one of the things I liked the most about Go. The standard library alone is very powerful and you can achieve most of the stuff just with it.

However, there was a little something that my mind keep thinking: it would be super good to have swagger docs out of the box like in C#. Then, when I started researching I found out two different approaches:

  1. Create your OpenAPI spec (swagger) manually and use a generator to do code scaffolding.
  2. Decorate your code endpoints with comments describing OpenAPI spec parameters.

It is not needed to say that the first option was something I did not even think of, as I seldom use code generators, limiting myself only to moq generators.

Both options will end up with the codebase being cohesive to the OpenAPI specification, but that is just either a .json or a .yaml file, how about serving it as an .HTML page?

Below, you can find a guide covering all the things needed to accomplish this quest without adding code dependencies.

👉 The complete example repository can be found on my Github. 👈

Swag

To generate the OpenAPI spec, one of the most suitable tools available is the CLI named swaggo/swag . As mentioned before, this tool will parse the comments that are decorating the code endpoints to generate the correct specification. One of the good things of this CLI is that it has the capability of parsing the request and response structures and populating them with values.

type petResponse struct {
	Id   int     `json:"id" example:"1"`
	Name string  `json:"name" example:"Fenrir"`
	Type petType `json:"type" example:"dog" enums:"dog,cat"`
}

type errorResponse struct {
	Message string `json:"message"`
}

// @summary Get pet by ID
// @description Gets a pet using the pet ID
// @id get-pet-by-id
// @produce json
// @Param id path int true "Pet ID"
// @Success 200 {object} main.petResponse
// @Success 400 {object} main.errorResponse
// @Success 404 {object} main.errorResponse
// @Success 405 {object} main.errorResponse
// @Router /api/pets/{id} [get]
// @tags Pets
func getPetByID() http.HandlerFunc {
	...
}

When the developer executes: swag init -g main.go, the resultant swagger.yaml will look like the following:

definitions:
  main.errorResponse:
    properties:
      message:
        type: string
    type: object
  main.petResponse:
    properties:
      id:
        example: 1
        type: integer
      name:
        example: Fenrir
        type: string
      type:
        enum:
        - dog
        - cat
        example: dog
        type: string
    type: object
paths:
  /api/pets/{id}:
    get:
      description: Gets a pet using the pet ID
      operationId: get-pet-by-id
      parameters:
      - description: Pet ID
        in: path
        name: id
        required: true
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: OK
          schema:
            $ref: '#/definitions/main.petResponse'
        "400":
          description: Bad Request
          schema:
            $ref: '#/definitions/main.errorResponse'
        "404":
          description: Not Found
          schema:
            $ref: '#/definitions/main.errorResponse'
        "405":
          description: Method Not Allowed
          schema:
            $ref: '#/definitions/main.errorResponse'
      summary: Get pet by ID
      tags:
      - Pets

Now, another good thing is that swag will be installed in the developer’s machine, so the code will not have a dependency.

Redoc

Redoc generates a zero dependency .HTML file from an OpenAPI specification. This utility has to be installed in the developer’s machine, requiring npm, but again, there is no code dependency on doing so. To generate the .HTML file from the spec:

redoc-cli bundle swagger.yaml -o swagger.html

👉 The resultant file will look like this. 👈

Go embed

There are many ways one can serve an static .HTML file on Go, but the one that caught my attention is using embed files, which is a feature available from Go 1.16.

Using this feature, the code will know that certain file, or files inside a directory, should be included in the resultant binary. This has many advantages in comparison to other approaches:

Dockerfile does not have to explicitly add the HTML file to the execution container:

FROM scratch
COPY --from=build-docs ./swagger.html /static/swagger.html <- NOT NEEDED
COPY --from=compiler /bin/app /app
ENTRYPOINT ["/app"]

In case the codebase is used as a dependency downloaded via go modules, the HTML file wont be downloaded. This is intended and also happens with migration files like .sql. Dependencies installed with go modules only download the *.go files that are in it. Embed can solve this as well:

library: generic-service
- file.go
	- //go:embed database/migrations/*
	- //go:embed server/static/index.html
- database
	- migrations
		- 01-up.sql
		- 01-down.sql
- server
	- static
    	- index.html

service: implementation-service
- main.go
- go.mod
    - generic-service
		- able to access the .sql and .html files

This can be verified doing go mod vendor on the implementation-service and checking that the mentioned files are included in the vendor folder.

How does it look then in the code?

func main() {

	router := http.NewServeMux()
    // Omitted some lines for brevity
	router.HandleFunc("/api/docs", docs())

	err := http.ListenAndServe(":8080", router)
	if err != nil {
		log.Fatal(err)
	}
}

//go:embed swagger/swagger.html
var swaggerHTML string

func docs() http.HandlerFunc {
	return func(w http.ResponseWriter, rq *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		_, _ = fmt.Fprint(w, swaggerHTML)
	}
}

In the example above, the swaggerHTML is populated with the content of the generated .HTML file; then every time the /api/docs endpoint is called, the HTML will be served from memory.

All in one

In order to simplify the process, a Makefile rule can be exposed like:

## Requires developer to install on his/her machine both swag and redoc-cli
## go install github.com/swaggo/swag/cmd/[email protected]
## npm install -g [email protected]
swagger/upd:
	swag init -g main.go -o ./swagger
	cd swagger && rm docs.go swagger.json
	cd swagger && redoc-cli bundle swagger.yaml -o swagger.html
  1. Generate the swagger.yaml file in a specific folder.
  2. Remove the docs.go and swagger.json files that are also generated by swag and are not needed.
  3. Generate the swagger.html file.

The result will be a folder containing both swagger.yaml and swagger.html. Then, the Makefile command can be invoked every time an endpoint is changed, or could potentially be added on a Git pre commit hook to ensure that the OpenAPI specification is always up to date.

👉 The complete example repository can be found on my Github. 👈