5 minutes
Go - Generate and serve swagger without code dependencies
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:
- Create your OpenAPI spec (swagger) manually and use a generator to do code scaffolding.
- 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
- Generate the
swagger.yaml
file in a specific folder. - Remove the
docs.go
andswagger.json
files that are also generated byswag
and are not needed. - 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. 👈