How to Integrate Swagger UI in Go Backend - Gin Edition
Tags: swagger, go, gin, openapi
Published: 2022-05-30 07:08:42

## 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 existing Gin server

You might consider [Building a Book Store API in Golang With Gin][1] 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:

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q2/sample-swagger-ui-documentation.png" alt="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][2]. 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:

```go
package main

import "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:

```diff
-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`.

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q2/failed-to-load-api-definition.png" alt="Docs loaded, 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.

```diff
 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][3]. We'll see more of these as we go, but for now, let's regenerate our docs and review the API docs.

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q2/swagger-ui-with-title-only.png" alt="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][3]. You can find them in use [in real life here][4].

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:

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q2/swagger-api-server-with-metadata.png" alt="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.

```diff
 {
     "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][5] 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][6] on each of the route handlers.

Here is modified `handlers/books.go`:

```diff
        "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:

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q2/swagger-ui-routes-documented.png" alt="Swagger UI; with documented routes">

And here is the updated `swagger.yaml`:

```diff
 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`.

```diff
 
 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.

[1]: /posts/2022/building-a-bookstore-api-in-golang-with-gin/
[2]: https://github.com/santosh/gingo/tree/91cafb5e19f3bbc3be9135ba74f8883d74941651
[3]: https://github.com/swaggo/swag#general-api-info
[4]: https://github.com/swaggo/swag/blob/master/example/celler/main.go#L16-L57
[5]: https://github.com/swaggo/swag#api-operation
[6]: https://github.com/swaggo/swag/tree/master/example/celler/controller