DEV Community

Cover image for How to Build and Document a Go REST API with Gin and Go-Swagger
Pieces 🌟 for Pieces.app

Posted on • Edited on • Originally published at code.pieces.app

How to Build and Document a Go REST API with Gin and Go-Swagger

In this blog post, we will go over what an API is, how to build a basic Go REST API using the Gin framework, and how to document the API using the go-swagger package.

We will also go over the importance of API documentation, best practices and API security considerations.

Prerequisites

To follow along, we will need to have the following:

What is an API?

The term API stands for Application Programming Interface. It refers to a programming language agnostic contract between systems that need to communicate and pass data around.

This communication usually happens over the internet via HTTPS. HTTPS is a protocol for moving data in a secure encrypted manner.

The API contract defines behavior and usage. API contracts typically base their definitions on pre-existing specifications such as the OpenAPI spec and the GraphQL schema spec.

APIs use various communication methodologies including Web Sockets, Remote Procedure Calls (RPC) and REST. In this blog, we will be focussing on and building out a REST API.

REST stands for Representational State Transfer and is a systems architecture style that defines a set of constraints for designing applications that rely on network connections.

RESTful APIs conform to these standards and are what sits between the application requesting data over the network and the data itself. The requesting application is referred to as the client, the data being requested is the resource and the application receiving the request is the server. This is the client-server model used by RESTful APIs.

The client requests the resource via standard HTTP methods commonly known as HTTP verbs. The verbs define what action the client wants to perform on the requested resource. HTTP verbs include:

  • GET - To request the resource
  • POST - To add data of the same shape as the resource to the server
  • PUT - To update a specified resource or create one if the resource is not there
  • DELETE - To delete a specified resource from the server
  • PATCH - To modify parts of an existing resource
  • HEAD - To retrieve the HTTP headers from the server response

This article goes deeper into REST, RESTful APIs and HTTP Verbs.

How To Build a Go REST API Using Gin

Now that we understand what an API is, let’s go ahead and build a basic blog API using the Go API framework. This blog API will expose a few endpoints, which are specific URIs (Uniform Resource Identifier), to the client.

These endpoints will correlate with the HTTP verbs and will be dealing with just the one Blog resource. The blog resource shape will contain a unique identifier or an ID, a title, a description, a body, an author and whether or not it is published.

First, we will need to create a folder or a directory somewhere on the computer. Then open this folder in a preferred text editor or IDE.

Open the integrated terminal in the text editor and paste the following:

go mod init blog-api
Enter fullscreen mode Exit fullscreen mode

This will initialize a Go project with “blog-api” being the name of the module. It will also create a go.mod file in the folder that will keep track of project information and any dependencies we might need.

Next create a main.go file, which will be the entry point of our application, and paste the following in it:

package main

func main() {}
Enter fullscreen mode Exit fullscreen mode

Then create a different folder within the same folder. Name it routes. Create a file called api.go in the routes folder and paste in the following code:

package routes

import (
    "fmt"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
)

type Blog struct {
    ID int `json:"id"`
    Title string `json:"title"`
    Description string `json:"description"`
    Body string `json:"body"`
    Author string `json:"author"`
    IsPublished bool `json:"isPublished"`
}
Enter fullscreen mode Exit fullscreen mode

This code puts our api.go file under the routes package and defines a struct of type Blog which contains an ID of type integer, a title, description and author of type string and an isPublished field of type boolean, which can only hold a true or false value.

Now paste the following code below the above snippet in the same file:

var blogs = []Blog{
    {
        ID:          1,
        Title:       "My first blog",
        Description: "This is my first blog",
        Body:        "This is the body of my first blog",
        Author:      "John Doe",
        IsPublished: true,
    },
    {
        ID:          2,
        Title:       "My second blog",
        Description: "This is my second blog",
        Body:        "This is the body of my second blog",
        Author:      "Jane Doe",
        IsPublished: false,
    },
    {
        ID:          3,
        Title:       "My third blog",
        Description: "This is my third blog",
        Body:        "This is the body of my third blog",
        Author:      "John Doe",
        IsPublished: true,
    },
    {
        ID:          4,
        Title:       "My fourth blog",
        Description: "This is my fourth blog",
        Body:        "This is the body of my fourth blog",
        Author:      "Jane Doe",
        IsPublished: true,
    },
}
Enter fullscreen mode Exit fullscreen mode

This adds some sample blogs in a slice. This is the resource we will be interacting with using our API to keep focus on our main goal of building and documenting the Go RESTful API. In a real world scenario, we would be interacting with a data store of some sort in order to persist our data.

Now let’s define the functions that will be called whenever a request hits our API. All the functions will be referencing the context provided by the Gin web framework. Paste the following code below the sample slice we just added to api.go:

func (b *Blog) GetBlogs(c *gin.Context) {
 c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data":              
blogs})
}
Enter fullscreen mode Exit fullscreen mode

This defines a GetBlogs function as a method of the Blog struct and that takes in a context from Gin. It then returns some JSON containing a HTTP status of OK and the sample blogs as the data.

func (b *Blog) GetBlog(c *gin.Context) {
    id := c.Param("id")

    intID, err := strconv.Atoi(id)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"status":  http.StatusNotFound, "message": err.Error()})
        return
    }

    //find blog whose id matched the param id
    for _, blog := range blogs {
        if blog.ID == intID {
            c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": blog})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "Blog not found"})
}
Enter fullscreen mode Exit fullscreen mode

This defines a GetBlog function that is similar to the GetBlogs function except that this one expects an id. It parses the id back into an integer then runs a loop to find the blog from our sample that matches that id. If found it returns it, if not it returns a not found error.

Now let’s add the functions for creating a new blog and adding it to our blogs as below:

func (b *Blog) CreateBlog(c *gin.Context) {
    var incomingBlog Blog

    incomingBlog.ID = len(blogs) + 1
    err := c.BindJSON(&incomingBlog)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": err.Error()})
        return
    }
    blogs = append(blogs, incomingBlog)

    c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated, "data": incomingBlog})
}
Enter fullscreen mode Exit fullscreen mode

Here we are expecting a blog resource, assigning it the next number higher than the length of our sample blogs as its ID, then parsing the JSON and appending it onto our slice of blogs. We then return a created status and the newly added blog back to the client. We send back an error message in case of any issues creating the new blog.

Now let’s add a function to update an existing blog:

func (b *Blog) UpdateBlog(c *gin.Context) {
    id := c.Param("id")

    // Convert the ID to an integer
    intID, err := strconv.Atoi(id)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": err.Error()})
        return
    }

    // Find the blog with the matching ID
    for index, blog := range blogs {
        if blog.ID == intID {
            // Parse the request body to get the updated blog data
            var updatedBlog Blog
            err := c.BindJSON(&updatedBlog)
            if err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": err.Error()})
                return
            }

            // Update the blog with the new data
            updatedBlog.ID = intID
            blogs[index] = updatedBlog

            // Respond with the updated blog
            c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": updatedBlog})
            return
        }
    }
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "Blog not found"})
}
Enter fullscreen mode Exit fullscreen mode

This code expects an id, parses it then finds the blog with the matching id. If successful it updates the blog with the changed blog that it received as part of the request body, then returns a status OK message with the updated blog. It also handles errors and sends back relevant messages in the case of any error.

Finally, let’s add a function to handle the deletion of a blog:

func (b *Blog) DeleteBlog(c *gin.Context) {
    id := c.Param("id")

    intID, err := strconv.Atoi(id)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": err.Error()})
    }

    for index, blog := range blogs {
        if blog.ID == intID {
            blogs = append(blogs[:index], blogs[index+1:]...)
            c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Blog deleted successfully"})
            return
        }
    }
    c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "Blog could not be deleted. Blog not found"})
}
Enter fullscreen mode Exit fullscreen mode

This function looks for the blog with the matching ID then removes it from the slice of blogs. It then sends back to the client a message that the blog was deleted successfully. It handles errors and sends appropriate responses in that case as well. This Pieces link contains the entire api.go file for easy viewing.

Back in main.go, paste the following code between the package statement at the top and the function main:

import (
    "blog-api/routes"
    "github.com/gin-gonic/gin"
  )
Enter fullscreen mode Exit fullscreen mode

This imports our routes folder and the Gin web framework. We need the Gin web framework as it makes spinning up a web server in Go approachable and is the go-to web framework in the Go eco-system.

Then paste the following in the main function in the main.go file:

   blog := &routes.Blog{}

    router := gin.Default()

    router.GET("/blogs", blog.GetBlogs)
    router.GET("/blogs/:id", blog.GetBlog)
    router.POST("/blogs", blog.CreateBlog)
    router.PUT("/blogs/:id", blog.UpdateBlog)
    router.DELETE("/blogs/:id", blog.DeleteBlog)

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

This code pulls in our Blog type and initializes the Gin web framework, which is now available in the router variable. It then defines a set of routes, the HTTP verb they expect and the handler function to be executed on each route. It then runs the server on port 8080.

For example, the router.POST line says that if the API gets a POST request on the “/blogs” endpoint then it will execute the UpdateBlog function defined in the api.go file.

How To Test The Go REST API Example Using ThunderClient

Our API is now ready for testing. We can use different API methods to test the functionality and behavior of the API including cURL and Postman. For this, we will use a vscode extension called ThunderClient. If using a different text editor, Postman is similar and can be used in place of ThunderClient.

To add ThunderClient to VS Code, click on the extensions icon in the toolbar and search it in the search bar. Click on the search result and install the extension. After installation, there should be a new ThunderClient icon in the toolbar. Click it to open the interface we will be using to send requests to our API.

Open the integrated terminal and run go run main.go to start our server. Then in ThunderClient, let’s paste this url “localhost:8080/blogs” in the bar like in the screenshot below:

Pasting a URL in VS Code.

Click “Send” to send the request to our API. We should get the list of blogs we defined in api.go back under the responses tab with a 200 OK status. Add the number 2 after the url and we should now get back only the blog with the id of 2 in the responses tab.

So far, we have tested the GET route for getting all resources and for getting a specific resource.

Change the HTTP verb via the dropdown to DELETE like so:

Changing the dropdown to Delete.

Then click on “Send”. We will get back a message saying that we successfully deleted the blog with id 2. Now let’s change it back to GET and click “Send”, this time we will get an error response saying that the blog was not found.

Let’s change the verb to POST and try to add a new blog entry to our list of blogs. Change the URL back to “localhost://8080/blogs” then paste the following in the body section of ThunderClient:

{
      "title": "My new blog",
      "description": "This is my new  blog",
      "body": "This is the body of my new blog",
      "author": "John Doe 2",
      "isPublished": false
 }
Enter fullscreen mode Exit fullscreen mode

As shown below:

The above code in VS Code.

Then click “Send” and we will get back our newly created blog and a created status of 201 meaning that our blog was added to the list of blogs we had defined in api.go.

Finally, let’s test the PUT method of our API. In ThunderClient, let’s change the verb to PUT and change the URL to “localhost:8080/blogs/1” then paste the following into the body section of ThunderClient:

{
  "title": "My first blog EVER",
  "description": "This is my first blog",
  "body": "This is the body of my first blog",
  "author": "John Doe",
  "isPublished": true
}
Enter fullscreen mode Exit fullscreen mode

Now when we click “Send” we get back the newly updated blog and a 200 OK status. We have now verified that our blog API works as expected.

How To Document The API With Go-Swagger

The next step is to document our Go REST API. But why should we document our API anyway?

Importance of API Documentation

Here are some reasons API documentation should never be ignored.

  • Helps other developers quickly understand the API. This makes onboarding new developers easier and they can be productive faster
  • Makes the API straightforward to version and maintain as it serves as the source of truth
  • Makes testing the API approachable and clear. This increases chances of the API being tested, increasing security

Documenting APIs also forces decisions regarding the behavior of the API to be well thought through, thereby reducing chances of regressions.

Documenting the API

There are several ways to document an API including in an API testing client like Postman, but we will use OpenAPI spec to document. OpenAPI is an evolution of Swagger, which is a popular open source tool for all things API. Swagger comes with a user interface to display, interact and test our API documentation.

In Go, there are several packages to help with our task including Swag but we will use the go-swagger package as it is mainstream, robust and allows us to separate our documentation from the handler functions definitions. This makes documentation readable for the developers who will maintain it.

To install go-swagger, run the following command in the terminal:

go get -u github.com/go-swagger/go-swagger/cmd/swagger
Enter fullscreen mode Exit fullscreen mode

This will get the package for us. Then in the routes folder, add a new file called api_docs.go to hold our documentations. It is important to co-locate this file as it needs to share a package name with the file it is documenting.

Paste the following code in the new file:

// Package routes Blog API.
//
//  Schemes: http
//  BasePath: /
//  Version: 1.0.0
//  Host: localhost:8080
//
//  Consumes:
//  - application/json
//
//  Produces:
//  - application/json
//
// swagger:meta
package routes
Enter fullscreen mode Exit fullscreen mode

This defines the api_docs file as part of the routes package and adds some metadata regarding our API. This includes the scheme it is using, the base path, version, the port it will be running on and the content-type it expects and returns.

Let’s add the following code below the package routes line in the same file:

func GetBlogs() {}
func GetBlog() {}
func CreateBlog() {}
func UpdateBlog() {}
func DeleteBlog() {}
Enter fullscreen mode Exit fullscreen mode

Notice that the functions have the same names as the ones in api.go file and that they are placeholders so that go-swagger can properly determine the relations between the functions and our comments.

Now we can add documentation to the placeholder functions in the api_docs file as below:

// swagger:route GET /blogs blogs getBlogs
//
// GetBlogs returns all blogs.
//
// Responses:
//
//  200: successResponse
func GetBlogs() {}

// swagger:route GET /blogs/{id} blogs getBlog
//
// GetBlog returns a blog by its ID.
//
// Responses:
//
//  200: successResponseR
//    400: errorResponse
func GetBlog() {}

// swagger:route POST /blogs blogs createBlog
//
// CreateBlog creates a new blog and returns it.
//
// Responses:
//
//  201: successResponse
//  400: errorResponse
func CreateBlog() {}

// swagger:route PUT /blogs/{id} blogs updateBlog
//
// UpdateBlog updates a blog by its ID.
//
// Responses:
//
//  200: successResponse
//  400: errorResponse
//  404: errorResponse
func UpdateBlog() {}

// swagger:route DELETE /blogs/{id} blogs deleteBlog
//
// DeleteBlog deletes a blog by its ID.
//
// Responses:
//
//  200: successResponse
//    404: errorResponse
func DeleteBlog() {}
Enter fullscreen mode Exit fullscreen mode

Here we are adding specific comments telling go-swagger that the function placeholders are tied to a route via “swagger:route”. We say the HTTP verb associated with each route, describe how the URL will look like, add a short explanation of what the function should do to the requested resource and state all the possible responses each route can return.

For the routes that expect parameters, we specify the name, type, whether it is a required parameter and whether we get it via the URL (path) or via request body. We will need to add more comments and definitions for the success and error responses referenced in the code above. This Pieces link contains the complete file with the rest of the documentation.

Now, in our api.go let’s replace the struct Blog type with this code that includes its definitions:

// Blog represents a blog post with a title, description, body, author, and publication status.
// swagger:model
type Blog struct {
    // The ID of the blog
    //
    // example: 1
    ID int `json:"id"`

    // The title of the blog
    // required: true
    // example: My first blog
    Title string `json:"title"`

    // The description of the blog
    //
    // example: This is my first blog
    Description string `json:"description"`

    // The body of the blog
    // required: true
    // example: This is the body of my first blog
    Body string `json:"body"`

    // The author of the blog
    // required: true
    // example: John Doe
    Author string `json:"author"`

    // The publication status of the blog
    // required: true
    // example: true
    IsPublished bool `json:"isPublished"`
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our definitions in place, we can ask go-swagger to generate our documentation by running the following command in a new terminal window:

swagger generate spec -o ./swagger.json
Enter fullscreen mode Exit fullscreen mode

This will generate a swagger.json file at the root of our application folder. If we open it, we will see that go-swagger took our code comments and turned it into json.

Let’s now serve Swagger UI to actually see this documentation and try to interact with it by running the following command:

swagger serve -F=swagger swagger.json
Enter fullscreen mode Exit fullscreen mode

This will output in the terminal a port that has our Swagger UI running. Click on the link and we should now be able to interact and see our documentation. It should look something similar to this:

The documentation live.

We can click on the dropdown arrows and test our API. The UI allows us to make calls to our API, tells us information about the parameters our endpoints expect and shows us the shape of the resource we will be interacting with. We can even see the error and success states..

But if we try to interact with the Swagger UI we will run into a CORS error as below:

A CORS error.

CORS refers to Cross-Origin Resource Sharing. It is a security feature in web browsers that determines how resources on different domains interact. The error in the screenshot above is because our server is running on port 8080 and our Swagger UI is not. The UI is making requests to our server from a different origin than the server itself and our server did not specify that this was allowed.

We can fix that by adding support for CORS in our main.go file. Let’s replace the import in main.go with the following:

import (
    "blog-api/routes"

    "github.com/gin-gonic/gin"
    cors "github.com/rs/cors/wrapper/gin"
)
Enter fullscreen mode Exit fullscreen mode

Then add the following code in the main function below the router := gin.Default() statement and above the route declarations:

    corsConfig := cors.New(cors.Options{
        AllowedOrigins:   []string{"*"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders:   []string{"Origin", "Content-Type"},
        AllowCredentials: true,
    })
    router.Use(corsConfig)
Enter fullscreen mode Exit fullscreen mode

This tells the browser that our server allows access from all domains and for all methods. This is what the main.go file should be like at this point.

Now, if we rerun our main.go file and try out our Swagger UI, we should get the proper response like so:

The file without errors.

We can go ahead and retest our API via the UI and verify that everything works as expected.

API Security Basics

There are some considerations we can take to make our API more secure and robust. For example, allowing our server to accept requests from any origin is not good practice. In reality, we restrict access to domains we know about.

Here are some security measures we can take to protect our API and our data:

Authentication and Authorisation

We could add an authentication layer to the API and expect client applications to identify and verify themselves before allowing access to our resources. This reduces chances of API abuse.

HTTPS

We could have our API only accessible via HTTPS which is encrypted and more secure than HTTP. This drastically increases the integrity of our API as it is harder to exploit and eavesdrop on information over an encrypted connection.

Rate Limiting

We could add a rate limit to our API that blocks access to our resources for a particular client after a certain number of requests in a set time period.This protects our API from abuse.

Wrapping Up

We have learned about building an API in Go (along with a Go Gin API example), and how to document it using go-swagger and OpenAPI spec. We have also learned the importance of API documentation and API security basics.

Resources

Looking to build your own Go REST API? Here are some helpful resources and further reading:

Top comments (0)