This tutorial covers:

  1. Building a RESTful API with Golang using the Gin-gonic framework
  2. Writing and running tests for endpoints
  3. Automating tests

Gin is a high-performance HTTP web framework written in Golang. It contains features and functionalities like routing and middleware out of the box. This helps to reduce boilerplate code, improves productivity, and simplifies the process of building microservices.

In this tutorial, I will guide you through the process of building a RESTful API with Golang using the Gin-gonic framework. I will also lead you through building an API to manage the basic details of a company. This API will allow you to create, edit, delete, and retrieve the list of companies.

For the sake of simplicity, I won’t cover data persistence in this tutorial. Instead, we will use a dummy list of companies that we can update or delete from accordingly. As simple as that might sound, it is enough to get you started with building robust API and unit testing with Golang.

Prerequisites

You will need the following to get the most out of this tutorial:

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Getting started

To begin, navigate to your development folder via the terminal and create a new folder for the project using the following commands:

mkdir golang-company-api
cd golang-company-api

The preceding command creates a folder named golang-company-api and navigates into it.

Next, initialize a Go module within the project by issuing the command:

go mod init golang-company-api

This creates a go.mod file where your project’s dependencies will be listed for tracking.

Installing the project’s dependencies

As mentioned, this project will use the Gin framework as an external dependency. Issue the following command from the root of your project to install the latest version of Gin and other dependencies:

go get -u github.com/gin-gonic/gin github.com/stretchr/testify github.com/rs/xid

Once the installation process is successful, you have access to Gin and the following packages within your application:

  • Testify is one of the most popular testing packages for Golang
  • XID is a globally unique id generator library.

Creating the homepage

Now, create a file named main.go within the root of the project. This will be the entry point of the application and will also house most of the functions that will be responsible for all functionalities. Open the new file and use the following content for it:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func HomepageHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message":"Welcome to the Tech Company listing API with Golang"})
}


func main() {
    router := gin.Default()
    router.GET("/", HomepageHandler)
    router.Run()
}

This code imports Gin and a net/http package that provides HTTP client and server implementations. It creates a HomepageHandler() method to handle responses on the homepage of your application.

Lastly, the main() function initializes a new Gin router, defines the HTTP verb for the homepage, and runs an HTTP server on the default port of 8080 by invoking the Run() of the Gin instance.

Run the project

Run the project:

go run main.go

This command runs the application on the default port 8080. Go to http://localhost:8080 to review it.

Application homepage

Now that the application works as expected, you can start implementing the required logic for the API endpoints. For now, stop the application from running using CTRL+C. Then press Enter.

Creating REST APIs

Before you go on, you need to define a data structure that will hold the information about a company. This will contain the properties and fields of a company. Each company will have an ID, a Name, the name of the CEO, and Revenue - estimated annual revenue generated by the company.

Defining the company model

Use a Go struct to define this model. Within the main.go file, declare the following struct:

type Company struct {
    ID     string  `json:"id"`
    Name  string  `json:"name"`
    CEO string  `json:"ceo"`
    Revenue  string `json:"revenue"`
}

To easily map each field to a specific name, specify the tags on each using backticks. This lets you send appropriate responses that fit into the JSON naming convention.

Defining a global variable

Next, define a global variable to represent companies and initialize the variable with a few dummy data. To the main.go file, just after the Company struct, add:

var companies = []Company{
    {ID: "1", Name: "Dell", CEO: "Michael Dell", Revenue: "92.2 billion"},
    {ID: "2", Name: "Netflix", CEO: "Reed Hastings", Revenue: "20.2 billion"},
    {ID: "3", Name: "Microsoft", CEO: "Satya Nadella", Revenue: "320 million"},
}

Creating a new company

Next, define the required logic to create a new company. Create a new method within the main.go file, call it *NewCompanyHandler* and use this code for it:

func NewCompanyHandler(c *gin.Context) {
    var newCompany Company
    if err := c.ShouldBindJSON(&newCompany); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    newCompany.ID = xid.New().String()
    companies = append(companies, newCompany)
    c.JSON(http.StatusCreated,  newCompany)
}

This code snippet binds the incoming request body into a Company struct instance and then specifies a unique ID. It appends the newCompany to the list of companies. If there is an error, it returns an error response, otherwise it returns a successful response.

Get the list of companies

To retrieve the list of companies, define a *GetCompaniesHandler* method:

func GetCompaniesHandler(c *gin.Context) {
    c.JSON(http.StatusOK, companies)
}

This uses the c.JSON() method to map the companies array into JSON and return it.

Update a company

To update the details of an existing company, define a method named *UpdateCompanyHandler* with this content:

func UpdateCompanyHandler(c *gin.Context) {
    id := c.Param("id")
    var company Company
    if err := c.ShouldBindJSON(&company); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    index := -1
    for i := 0; i < len(companies); i++ {
        if companies[i].ID == id {
            index = 1
        }
    }
    if index == -1 {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "Company not found",
        })
        return
    }
    companies[index] = company
    c.JSON(http.StatusOK, company)
}

This snippet uses the c.Param() method to fetch the company’s unique id from the request URL. It checks that the record exists in the company’s list or not and then updates the specified company accordingly.

Delete a company

Create a *DeleteCompanyHandler* method with this content:

func DeleteCompanyHandler(c *gin.Context) {
    id := c.Param("id")
    index := -1
    for i := 0; i < len(companies); i++ {
        if companies[i].ID == id {
            index = 1
        }
    }
    if index == -1 {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "Company not found",
        })
        return
    }
    companies = append(companies[:index], companies[index+1:]...)
    c.JSON(http.StatusOK, gin.H{
        "message": "Company has been deleted",
    })
}

Similar to the *UpdateCompanyHandler*, the method from this snippet uses the unique identifier to target the details of the company that needs to be removed from the list. It deletes the company details, and returns a successful response.

Setting up API Route handler

Next, register all the appropriate endpoints and map them to the methods defined earlier. Update the main() as shown here:

func main() {
    router := gin.Default()
    router.GET("/", HomepageHandler)
    router.GET("/companies", GetCompaniesHandler)
    router.POST("/company", NewCompanyHandler)
    router.PUT("/company/:id", UpdateCompanyHandler)
    router.DELETE("/company/:id", DeleteCompanyHandler)
    router.Run()
}

This would have been updated if you were using a code editor or IDE that supports automatic imports of packages. If you are not using that type of editor or IDE, make sure that the import matches this snippet:

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "github.com/rs/xid"
)

Testing the application

Within the required methods defined and individual endpoints registered, go back to the terminal and run the application again using go run main.go. This will start the application on port 8080

Creating a new company

Test this out using Postman or your preferred API testing tool. Send an HTTP POST request to http://localhost:8080/company. Use the data below as the request payload:

{
  "name":"Shrima Pizza",
  "ceo": "Demo CEO",
  "revenue":"300 million"
}

Create a new company

Retrieving the list of companies

To retrieve the list of companies, set an HTTP GET request to http://localhost:8080/companies.

Retrieve companies

Writing tests for the endpoints

Now that your application is working as expected, focus on writing unit tests for all the methods created to handle the logic for your API endpoints.

Golang out of the box comes installed with a testing package that makes it easier to write tests. To begin, create a file named main_test.go and populate it with this:

package main
import "github.com/gin-gonic/gin"

func SetUpRouter() *gin.Engine{
    router := gin.Default()
    return router
}

This is a method to return an instance of the Gin router. It will come in handy when testing other functions for each endpoint.

Note: Each test file within your project must end with _test.go and each test method must start with a Test prefix. This is a standard naming convention for a valid test.

Testing homepage response

In the main_test.go file, define a *TestHomepageHandler* method and use this code:

func TestHomepageHandler(t *testing.T) {
    mockResponse := `{"message":"Welcome to the Tech Company listing API with Golang"}`
    r := SetUpRouter()
    r.GET("/", HomepageHandler)
    req, _ := http.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    responseData, _ := ioutil.ReadAll(w.Body)
    assert.Equal(t, mockResponse, string(responseData))
    assert.Equal(t, http.StatusOK, w.Code)
}

This test script sets up a server using the Gin engine and issues a GET request to the homepage /. It then uses the assert property from the testify package to check the status code and response payload.

Testing create new company endpoint

To test the /company endpoint for your API, create a *TestNewCompanyHandler* method and use this code for it:

func TestNewCompanyHandler(t *testing.T) {
    r := SetUpRouter()
    r.POST("/company", NewCompanyHandler)
    companyId := xid.New().String()
    company := Company{
        ID: companyId,
        Name: "Demo Company",
        CEO: "Demo CEO",
        Revenue: "35 million",
    }
    jsonValue, _ := json.Marshal(company)
    req, _ := http.NewRequest("POST", "/company", bytes.NewBuffer(jsonValue))

    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    assert.Equal(t, http.StatusCreated, w.Code)
}

This code snippet issues a POST request with a sample payload and checks whether the returned response code is 201 StatusCreated.

Testing the get companies endpoint

Next is the method to test GET /companies resource. Define the *TestGetCompaniesHandler* method with this:

func TestGetCompaniesHandler(t *testing.T) {
    r := SetUpRouter()
    r.GET("/companies", GetCompaniesHandler)
    req, _ := http.NewRequest("GET", "/companies", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    var companies []Company
    json.Unmarshal(w.Body.Bytes(), &companies)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.NotEmpty(t, companies)
}

This code issues a GET request to the /companies endpoint and ensures that the returned payload is not empty. It also asserts that the status code is 200.

Testing the update company endpoint

The last test is for the HTTP handler responsible for updating a company’s details. In the main_test.go file use this code snippet:

func TestUpdateCompanyHandler(t *testing.T) {
    r := SetUpRouter()
    r.PUT("/company/:id", UpdateCompanyHandler)
    company := Company{
        ID: `2`,
        Name: "Demo Company",
        CEO: "Demo CEO",
        Revenue: "35 million",
    }
    jsonValue, _ := json.Marshal(company)
    reqFound, _ := http.NewRequest("PUT", "/company/"+company.ID, bytes.NewBuffer(jsonValue))
    w := httptest.NewRecorder()
    r.ServeHTTP(w, reqFound)
    assert.Equal(t, http.StatusOK, w.Code)

    reqNotFound, _ := http.NewRequest("PUT", "/company/12", bytes.NewBuffer(jsonValue))
    w = httptest.NewRecorder()
    r.ServeHTTP(w, reqNotFound)
    assert.Equal(t, http.StatusNotFound, w.Code)
}

This sent two HTTP PUT requests to the company/:id endpoint. One has a payload and a valid company ID and the other has an ID that does not exist. The valid call will return a successful response code while the invalid one responds with StatusNotFound.

Update the import section within the main_test.go file:

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/rs/xid"
    "github.com/stretchr/testify/assert"
)

Running the test locally

Now, run the test by issuing this command:

go test

Test out non verbose

To disable the Gin debug logs and enable verbose mode, run the command with a -V flag:

GIN_MODE=release go test -v

Testing output verbose

Automating the tests

Automate the test by creating a continuous integration pipeline on CircleCI. To add the required configuration, create a folder called .circleci, and in it, create a new file named config.yml. Open the new file and paste this code into it:

version: 2.1
orbs:
  go: circleci/go@1.7.1
jobs:
  build:
    executor:
      name: go/default
      tag: "1.16"
    steps:
      - checkout
      - go/load-cache
      - go/mod-download
      - go/save-cache
      - run:
          name: Run tests
          command: go test -v

This script pulls in the Go orb for CircleCI. This orb allows common Go-related tasks such as installing Go, downloading modules and caching to be carried out. It then checks out of the remote repository, and issues the command to run our test.

Next, set up a repository on GitHub and link the project to CircleCI. Review pushing your project to GitHub for instructions.

Connecting to CircleCI

Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.

Set up project

Click the Set Up Project button. You will be prompted about whether you have already defined the configuration file for CircleCI within your project. Enter the branch name (for the tutorial, we are using main). Click the Set Up Project button to complete the process.

Select config

By clicking a job in the workflow, you can view all the steps.

Select config

You can get further details of a job by clicking it - the Run tests job for example.

Pipeline output

There you have it!

Conclusion

With over 50k stars on GitHub, it’s amazing how Gin is gaining more popularity and gradually becoming a top choice for building efficient APIs among Golang developers.

In this tutorial, I have shown you how to build a REST API using Golang and Gin. I led you through writing a unit test for each endpoint, and setting up a continuous integration pipeline for it using GitHub and CircleCI. I hope you can apply what you have learned to your team’s projects.

Explore the code for the sample project here on GitHub.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile. A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.