How to Integrate Swagger UI in Go Backend — Gin Edition

Introduction

Unlike FastAPI, Gin does not have OpenAPI integration built in. With FastAPI when you add a route, documentation is already generated. With Gin, this is not the case. But recently I integrated Swagger UI in one of my Go backends and I wanted to document that process.

Prerequisites

  • An already running Gin server

You might consider Building a Book Store API in Golang With Gin if you are starting from scratch. I’ll be using the same book store and extending over it to integrate Swagger UI.

How Swagger works with Gin

The way Swagger works with other Go backend is not different they all have the same mechanism. But how does it really work and what do we have to really do?

Swagger uses the Go comment system which is very well integrated with documentation already as we know. We write comments in a pre-defined way, details of which we will see further ahead in the post. But mostly it is divided into 2 parts. The server itself and the routes.

Swagger has a CLI binary which when runs converts these comment documents into OpenAPI compliant documentation. The resultant file also includes OpenAPI server specs in JSON and YAML format.

Lastly, we route the generated content via a handler in our backend.

How do we do that? Let’s see.

Setup Swagger CLI

The prime repository you should keep under your pillow is https://github.com/swaggo/swag. Both in terms of CLI and documentation.

We’ll install a CLI application called swag. Here's how:

go get -u github.com/swaggo/swag/cmd/swag# 1.16 or newer
go install github.com/swaggo/swag/cmd/swag@latest

The first command will download the dependencies to integrate in the server application.

The second one is where we install the CLI.

Now you should be able to run the swag command:

$ swag -h
NAME:
swag - Automatically generate RESTful API documentation with Swagger 2.0 for Go.
USAGE:
swag [global options] command [command options] [arguments...]
VERSION:
v1.8.1
COMMANDS:
init, i Create docs.go
fmt, f format swag comments
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help (default: false)
--version, -v print the version (default: false)

Documenting the Gin Server

When you look at Swagger UI documentation for any OpenAPI enabled website, you’d see something like this:

Petstore Swagger UI Docs

You might be familiar with the low half part of the UI. This is what Swagger docs are for i.e. to document the routes.

But before we deal with the lower 50% part I want you to notice in the above picture is the top 50% part. Right from the Swagger Petstore header to Find out more about the Swagger anchor link.

This part shows the metadata about the API server itself. In this section, we are going to build that part. You can find the code to start with here. We’ll be continuing on that code.

First of all, we need to go get the packages we need to work with:

go get -u github.com/swaggo/files
go get -u github.com/swaggo/gin-swagger

Now after we have done that, let's look at our main.go file. This is the main.go at the moment:

package mainimport "github.com/santosh/gingo/routes"func main() {
router := routes.SetupRouter()
router.Run(":8080")
}

Add a route for Swagger Docs

We add a new router in the main function:

router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

The modified file would look like this:

-import "github.com/santosh/gingo/routes"
+import (
+ "github.com/santosh/gingo/routes"
+ swaggerFiles "github.com/swaggo/files"
+ ginSwagger "github.com/swaggo/gin-swagger"
+)

func main() {
router := routes.SetupRouter()

+ router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+
router.Run(":8080")
}

swaggerFiles.Handler here is the handler which has all the assets embedded in it. Assets like the HTML, CSS, and javascript files are shown on the documentation end.

ginSwagger.WrapHandler is a wrapper around http.Handler, but for Gin.

This should be enough for testing. Let’s see how the docs render at <server address>/docs/index.html.

Docs loaded, but API definition didn’t load

The good news is API documentation site is up. The bad news is it does not look like a normal API documentation site. What did we miss?

Generate swagger docs every time you modify doc string

Swagger documentation is stored in Go’s own docstring. We have a special syntax that we follow. More on that later. But first, let us learn how to generate docs.

swag init
2022/05/25 23:59:16 Generate swagger docs....
2022/05/25 23:59:16 Generate general API Info, search dir:./
2022/05/25 23:59:16 create docs.go at docs/docs.go
2022/05/25 23:59:16 create swagger.json at docs/swagger.json
2022/05/25 23:59:16 create swagger.yaml at docs/swagger.yaml

We run swag init every time we update docs for our API. This generates 3 files inside a sub directory called docs/.

$ tree docs
docs
├── docs.go
├── swagger.json
└── swagger.yaml
0 directories, 3 files

swagger.json and swagger.yaml are the actual specification which you can upload to services like AWS API Gateway and similar services. docs.go is a glue code that we need to import into our server.

Let us now import the documentation to our main.go.

package main

import (
+ _ "github.com/santosh/gingo/docs"
"github.com/santosh/gingo/routes"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)

+// @title Gingo Bookstore API
func main() {
router := routes.SetupRouter()

As you can see we have imported docs the module by giving the full path of the module. We also have to prepend this import using _ because it is not explicitly used in main.go file.

You might also have noticed // @title Gingo Bookstore API line. Please note its position as this is important. It is just above the main() func. Also @title is not random here. It is one of the keywords which is documented under General API Info. We'll see more of these as we go, but for now, let's regenerate our docs and review the API docs.

Working API Docs

Looks like we are getting somewhere. :D

Add General API Info to Gin API Server

We saw @title annotation in the last section. It is used to set the title for the API server. But it is not the only annotation available. There is a wide array of annotation in Swagger. You can find them in use in real life here.

I’m going to use some of them to construct metadata on my doc site.

// @title           Gin Book Service
// @version 1.0
// @description A book management service API in Go using Gin framework.
// @termsOfService https://tos.santoshk.dev
// @contact.name Santosh Kumar
// @contact.url https://twitter.com/sntshk
// @contact.email sntshkmr60@gmail.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
  1. Run swag init .
  2. Re-run the server.
  3. Check for update:
Swagger API Server with Metadata

That’s some metadata in there.

You might also have noticed that the API spec inside docs/ directory has changed. For example, here is the swagger.yaml file from the docs/ dir.

{
"swagger": "2.0",
"info": {
- "title": "Gingo Bookstore API",
- "contact": {}
+ "description": "A book management service API in Go using Gin framework.",
+ "title": "Gin Book Service",
+ "termsOfService": "https://tos.santoshk.dev",
+ "contact": {
+ "name": "Santosh Kumar",
+ "url": "https://twitter.com/sntshk",
+ "email": "sntshkmr60@gmail.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0"
},
+ "host": "localhost:8080",
+ "basePath": "/api/v1",
"paths": {}
}

Now it’s time to add docs for API endpoints.

Add API Operation Info to API Endpoints

Just like General API Info for the main server, there also is API Operation for individual routes/endpoints handlers.

They are more diverse than what I’m going to use here. The ones I’m going to use are just a subset of them. If you want to check out some real examples, you can find them here on each of the route handlers.

Here is modified handlers/books.go:

"github.com/santosh/gingo/models"
)

-// GetBooks responds with the list of all books as JSON.
+// GetBooks godoc
+// @Summary Get books array
+// @Description Responds with the list of all books as JSON.
+// @Tags books
+// @Produce json
+// @Success 200 {array} models.Book
+// @Router /books [get]
func GetBooks(c *gin.Context) {
c.JSON(http.StatusOK, db.Books)
}

-// PostBook takes a book JSON and store in DB.
+// PostBook godoc
+// @Summary Store a new book
+// @Description Takes a book JSON and store in DB. Return saved JSON.
+// @Tags books
+// @Produce json
+// @Param book body models.Book true "Book JSON"
+// @Success 200 {object} models.Book
+// @Router /books [post]
func PostBook(c *gin.Context) {
var newBook models.Book

@@ -28,7 +41,14 @@ func PostBook(c *gin.Context) {
c.JSON(http.StatusCreated, newBook)
}

-// GetBookByISBN locates the book whose ISBN value matches the isbn
+// GetBookByISBN godoc
+// @Summary Get single book by isbn
+// @Description Returns the book whose ISBN value matches the isbn.
+// @Tags books
+// @Produce json
+// @Param isbn path string true "search book by isbn"
+// @Success 200 {object} models.Book
+// @Router /books/{isbn} [get]
func GetBookByISBN(c *gin.Context) {
isbn := c.Param("isbn")
  1. Run swag init .
  2. Re-run the server.
  3. Check for updates:
Swagger UI; with documented routes

And here is the updated swagger.yaml:

basePath: /api/v1
+definitions:
+ models.Book:
+ properties:
+ author:
+ type: string
+ isbn:
+ type: string
+ title:
+ type: string
+ type: object
host: localhost:8080
info:
contact:
@@ -12,5 +22,58 @@ info:
termsOfService: https://tos.santoshk.dev
title: Gin Book Service
version: "1.0"
-paths: {}
+paths:
+ /books:
+ get:
+ description: Responds with the list of all books as JSON.
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/models.Book'
+ type: array
+ summary: Get books array
+ tags:
+ - books
+ post:
+ description: Takes a book JSON and store in DB. Return saved JSON.
+ parameters:
+ - description: Book JSON
+ in: body
+ name: book
+ required: true
+ schema:
+ $ref: '#/definitions/models.Book'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/models.Book'
+ summary: Store a new book
+ tags:
+ - books
+ /books/{isbn}:
+ get:
+ description: Returns the book whose ISBN value matches the isbn.
+ parameters:
+ - description: search book by isbn
+ in: path
+ name: isbn
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/models.Book'
+ summary: Get single book by isbn
+ tags:
+ - books
swagger: "2.0"

Notice the new definitions and paths section added. It is for models and the endpoints respectively.

Bonus: Router Group for /api/v1

It is always a good idea to prepend your routes with api/v1. v1 bit has a logic behind it. It is for the time when you have to introduce a breaking change which is not backward compatible. Maybe the input or the output has changes that might break millions of dependent client.

In these situations, you increment the version to something like api/v2 and let the older API server serve old from the old handler.

Right now all of the routes in gingo server start with /book. We are going to change that.

Here is the modified routes/routes.go.

func SetupRouter() *gin.Engine {
router := gin.Default()
- router.GET("/books", handlers.GetBooks)
- router.GET("/books/:isbn", handlers.GetBookByISBN)
- // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
- // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
- router.POST("/books", handlers.PostBook)
+
+ v1 := router.Group("/api/v1")
+ {
+ v1.GET("/books", handlers.GetBooks)
+ v1.GET("/books/:isbn", handlers.GetBookByISBN)
+ // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
+ // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
+ v1.POST("/books", handlers.PostBook)
+ }

return router
}

Here we use router.Group to create a group with a path of /api/v1. We then move all the route definitions into the group and surround it with braces. That is how we create a path route in Gin.

Conclusion

API documentation is an essential part of API documentation. Instead of documenting the endpoints anywhere else, we can document the routes right in the code. That way we only have 1 single source of truth. No need to maintain code and documentation separately. In turn, we get always up-to-date documentation.

Every backend server has some sort of support for Swagger UI. I have covered the basics for Gin, but if you use any other framework, I encourage you to look for your own framework.

Originally published at https://santoshk.dev on May 26, 2022.

--

--

Full Stack Developer. Pythonist. Gopher.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store