DEV Community

Cover image for How to Integrate Swagger UI in Go Backend - Gin Edition
Santosh Kumar
Santosh Kumar

Posted on

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 existing 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
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

package main

import "github.com/santosh/gingo/routes"

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

    router.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

Add a route for Swagger Docs

We add a new router in the main function:

router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
Enter fullscreen mode Exit fullscreen mode

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")
 }
Enter fullscreen mode Exit fullscreen mode

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, 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  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": {}
 }
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode
  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"
Enter fullscreen mode Exit fullscreen mode

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
 }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)