Build RESTful API using Go Gin [In-Depth Tutorial]

Getting Started with Go Gin

Gin is a fast and high performance HTTP web framework written in Go. It features a Martini-like API and it is 40 times faster than Martini. Below are the goodies that come with Go Gin.

  1. It is fast: It has small memory footprint, no reflection and the has predictable API performance
  2. Middleware support: Incoming requests from the client can be handled by many middleware in a chain. For example you can add logging, authorization middleware just to mention a few
  3. Crash free: During a panic on a HTTP request, Gin can catch and recover from the panic. This makes the server always available.  The panic events can be reported to Sentry for example
  4. JSON validation: Incoming JSON data from the client can be parsed and validated against the expected JSON data.
  5. Routes grouping: Your route code can be organized into groups hence making the code easy to read. For example, different version of you API routes can be put in a group i.e /api/v1 and /api/v2 groups
  6. Error management: Gin provides a convenient way to collect all errors that occur during HTTP requests. These error messages can be logged in an error log file.
  7. Rendering built in: You can render JSON, XML and HTML
  8. Extendable: Gin allows you to add your own middlewares

 

Prerequisite

  • Go version 1.16 and above
  • Code editor, VS Code is what I use.
  • The curl tool

 

Application structure in my Environment

Build RESTful API using Go Gin [In-Depth Tutorial]

The above image shows what this API will look like.

Advertisement
  • api package: This package will host code for all the CRUD operations routed by Gin
  • database package: This package will host code data model , and PostgresDB operation and database CRUD operation methods
  • main.go file: Entry point to our API. It will call the route handler function and run the Gin server.

Follow the below steps using the terminal in your work space to create the above application structure.

Create application folder and move into it

$ mkdir go-gin-api  && cd go-gin-api

Create the api and database packages

$ mkdir api database

Create main.go and .env file

$ touch main.go .env

Initialize application module

$ go mod init example.com/go-gin-api

Install the required packages i.e gin and godotenv

Advertisement
$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/joho/godotenv

 

Setting up Gin server

In this section, we are going to create a simple Gin server and create a route that returns back a string to the caller. We will also write code to run the server.

main.go 

package main
 
import (
   "net/http"
 
   "github.com/gin-gonic/gin"
)
 
func main() {
   r := gin.Default()
 
   r.GET("/", func(c *gin.Context) {
       c.JSON(http.StatusOK, gin.H{
           "message": "Welcome to Building RESTful API using Gin and Gorm",
       })
   })
 
   r.Run(":5000")
}

Output

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)
 
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000
[GIN] 2022/11/19 - 10:12:01 | 200 |      28.607µs |       127.0.0.1 | GET      "/"

Explanation

The above code shows how to set up a Gin server and run it. We start off by creating a new Gin engine using the r := gin.Default() statement.  The r variable is now a Gin Engine that exposes HTTP methods such as GET, PUT, POST DELETE. In the above example, we make use of the GET HTTP method to return JSON data to the client.  The GET method takes a string value i.e “/” which indicates the route that will generate the HTTP event . The GET method takes a second argument, an anonymous function that takes a pointer to a Gin context (*gin.Context). In the function body, we use the context to return to the client a JSON response with a status code http.StatusOK (code 200) to the client.

To make a GET request go to your browser and add http://127.0.0.1:5000/ in the URL bar and hit the enter button on your keyboard. A message from the server will appear in the browser.

 

Setting up the database

We are building an API that exposes articles written by different authors. Each article will have an id, title, description, and rating. The database package will be used to create articles models and all utilities for the articles model.

Using your terminal, move to the root directory where the main.go file is located and move to the database package and create a postgres.go file as shown below

$ cd database && touch postgres.go

Before doing anything , we first need to install the required packages for our database such as the GORM package. GORM is a fully featured Object Relational Mapper (ORM)  that is developer friendly built on top of the database/sql package. We will use GORM with PostgreSQL DB dialect.

We will also need to install the github.com/joho/godotenv package that will help us get the required database configurations such as database host, port, user, name and password . Issue the below command to install both GORM and github.com/joho/godotenv packages.

$ go get github.com/jinzhu/gorm
$ go get github.com/joho/godotenv

Next create an article model.This model defines the fields that an article will have such as the id, title, description and rate. After defining the database model, we will set up our postgreSQL DB using GORM. Add the article model and database set up  in the postgres.go file.

database/postgres.go

Advertisement
package database
 
import (
   "errors"
   "fmt"
   "log"
   "os"
 
   "github.com/jinzhu/gorm"
   _ "github.com/jinzhu/gorm/dialects/postgres"
   "github.com/joho/godotenv"
)
 
var db *gorm.DB
var err error
 
type Article struct {
   ID          uint   `json:"id" gorm:"primary_key"`
   Title       string `json:"title"`
   Description string `json:"description"`
   Rate        int    `json:"rate"`
}
 
func getEnvVariable(key string) string {
   err := godotenv.Load(".env")
   if err != nil {
       log.Fatal("Error loading .env file", err)
   }
   return os.Getenv(key)
}
 
func NewPostgreSQLClient() {
   var (
       host     = getEnvVariable("DB_HOST")
       port     = getEnvVariable("DB_PORT")
       user     = getEnvVariable("DB_USER")
       dbname   = getEnvVariable("DB_NAME")
       password = getEnvVariable("DB_PASSWORD")
   )
 
   conn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
       host,
       port,
       user,
       dbname,
       password,
   )
 
   db, err = gorm.Open("postgres", conn)
   if err != nil {
       log.Fatal(err)
   }
 
   db.AutoMigrate(Article{})
}

Dependencies

This file defines all database interaction with our data. We start of by import dependencies such as github.com/jinzhu/gorm, github.com/jinzhu/gorm/dialects/postgres and github.com/joho/godotenv responsible for Object Relational Mappers, PostgreSQL dialect and environmental variable importation respectively.

We then declare database and err variables using the statement var db *gorm.DB and var err error. The db variable  will be responsible for interacting with the database while the err variable will be responsible for storing errors that might occur while creating database connection.

Article Model

In the next statement, we create an Article model that defines the fields for each field in an article. It also has annotations for each field which will be useful while interacting with data from different clients like curl.

 

Environmental variables

We use the getEnvVariable() function to get environmental variables from the .env file. getEnvVariable() that takes a key of string type and returns a string. This function is responsible for getting database configurations from the operating system environment. But how do we load these environmental variables into the operating system?

Advertisement

This is where the <b>.env</b> file comes in.The .env files store environmental variables that are not supposed to be exposed to the public. This file should not be committed in a public repository either as this will give hackers access to your database in a production environment. Think of it as a file that stores credentials for our application such as passwords, secret keys and so on. In our example, we are using a .env file to store database configurations. Create a .env file in the root directory next to the main.go file and add the below statements.

.env

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_NAME=postgres
DB_PASSWORD=pass1234

Please replace the above configurations such as DB_PASSWORD, DB_NAME,and DB_UER to match with your configurations for postgreSQL DB. The DB_HOST and DB_PORT can remain as defined in the .env file.

Now let's go back to the database/postgres.go file. The next function defined in this file is the newPostgresDB() that returns to the caller an instance of a postgreSQL db and an error if any. In the function we define the required database variables using the var (host= get.EnvVariable …..). We then create a string that defines a connection to the database using the conn := fmt.Sprint() statement passing in the host, port, user dbname and password.

Next we define an open connection to the database using the statement db, err := gorm.Open(“postgres”, conn) . The gorm.Open() id responsible for :

  1. Look up the drivers for the given dialect. In our case “postgres” is our dialect.
  2. Call sql.Open() to return DB object
  3. Call DB.Ping() to force it to talk to the database

The second argument passed to the gorm.Open() method is the database connection string.

Advertisement

In the next line the statement db.AutoMigrate(Article{}) migrate our Article{} schema which translates to creating a database table called Article and  creating a database foreign key constraints automatically.

 

Setting up database CRUD functions

After a successful database setup, we write code that will handle GET, POST, PUT, DELETE operations.

 

Create article

We define a function called createArticle that is responsible for adding a new article into the Article table in the database. It receives data from the caller and writes the data into the database.

func CreateArticle(a *Article) (*Article, error) {
   res := db.Create(a)
   if res.RowsAffected == 0 {
       return &Article{}, errors.New("article not created")
   }
   return a, nil
}

The CreateArticle() function, takes a pointer to an article and returns a created article back to the caller or an error if write operation is  not successful. The db variable has a Create() method that takes an article and writes it into the Article table in the database. The res := db.Create() statement returns a database object which is used to check if data has been written into the database using the statement res.RowsAffected. If there number of rows affected is , this means that the data has not been written into the database hence returning  an error to the caller using the statement return &Article{}, errors.New(“article not created”)

 

Read Article

The read function is responsible for returning an article to the caller with a specific id. If the article is found in the database we return the article as a pointer hence return an error.

func ReadArticle(id string) (*Article, error) {
   var article Article
   res := db.First(&article, id)
   if res.RowsAffected == 0 {
       return nil, errors.New("article not found")
   }
   return &article, nil
}

The ReadArtcile function takes an id of type string and returns a pointer to an Article and an error. In the function , we declare a variable article of type Article using the statement var article Article.  In the next line, we user the db variable to get an article with the given id using the statement res := db.First(&article, id). If an article is not found we return an error message to the caller , else we return the found article.

Advertisement

 

Read All Articles

This function is responsible for reading all articles from the Article table in the database. This function returns all the articles to the caller.

func ReadArticles() ([]*Article, error) {
   var articles []*Article
   res := db.Find(&articles)
   if res.Error != nil {
       return nil, errors.New("authors not found")
   }
   return articles, nil
}

The ReadArticles function returns a pointer to an array of articles to the caller. We first of all define an articles array and use the db.Find(&articles) to read all articles from the database.

 

Update Article

This function is responsible for getting updated article data from the client, finds the article to update in the database and updates it with the new data.

func UpdateArticle(article *Article) (*Article, error) {
 
   var updateArticle Article
   result := db.Model(&updateArticle).Where(article.ID).Updates(article)
   if result.RowsAffected == 0 {
       return &Article{}, errors.New("artcile not updated")
   }
   return &updateArticle, nil
}

The UpdateArticle  function takes as an argument, the article to update as a pointer and returns an updated article to the caller if the update operation is successful else returns an error. We declare a updateArticle variable which will store the final data. In the next line, we update the article using the statement result := db.Model(&updateArticle).Where(article.ID).Updates(article).

 

Delete Article

This function is responsible for deleting an article with a given id from the database. It returns an error to the caller if the delete operation is not successful.

func DeleteArticle(id string) error {
   var deleteArticle Article
   result := db.Where(id).Delete(&deleteArticle)
   if result.RowsAffected == 0 {
       return errors.New("article data not deleted")
   }
   return nil
}

To delete an article, declare the deleteArticle variable and use the db.Where(id).Delete(&deleteArticle) statement to delete the article. If the delete operation is not successful , we return an error to the else return nil.

 

Setting up CRUD Routes handlers

In this section , we are going to make use of the Gin package to route HTTP requests from the clients to the database backend. Navigate to your api/gin.go file and add the following code.

api/gin.go

package api
 
import (
   "net/http"
 
   "example.com/go-gin-api/database"
   "github.com/gin-gonic/gin"
)

In this package import the net/http package to give us access to HTTP status codes such as 200, 500 , 201 and 404. We also import all the database CRUD functions from the example.com/go-gin-api/database package. Lastly import the Gin package to help us expose different HTTP methods and route the request appropriately.

 

Performing CRUD operations

Home Handler

This route handler is responsible for routing requests on the home URL i.e “/” . The home route maps request for http://127.0.0.1:5000/api/v1/articles.

func home(c *gin.Context) {
   c.JSON(http.StatusOK, gin.H{
       "message": "Welcome to Building RESTful API using Gin and Gorm",
   })
   return
}

 

Create Article Handler

This handler is responsible for getting from the client , parse the data and store the data into the database

func postArticle(c *gin.Context) {
   var article database.Article
   if err := c.ShouldBindJSON(&article); err != nil {
       c.JSON(http.StatusInternalServerError
, gin.H{
           "error": err,
       })
       return
   }
   res, err := database.CreateArticle(&article)
   if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }
 
   c.JSON(http.StatusCreated, gin.H{
       "article": res,
   })
   return
}

The postArticle handler function, takes a pointer to the gin.Context , the context then exposes methods such as JSON() and ShouldBindJSON(). We first declare an article of type Article using which will be binded by the incoming data from the client. This variable will be used to validate the incoming data. If the data binding is not successful we return an error to the client. If the bind is successful, we call the CreateArticle method from the database package using the statement res, err := database.CreateArticle(&article). If this operation is not successful, we return an error to the caller, return a successful response to the caller with the created article.

 

Read Article Handler

This function is responsible for getting an id from the request parameter and pulling an article with the given id  from the database.

func getArticle(c *gin.Context) {
   id := c.Param("id")
   article, err := database.ReadArticle(id)
 
   if err != nil {
       c.JSON(http.StatusNotFound, gin.H{
           "message": "article not found",
       })
       return
   }
   c.JSON(http.StatusOK, gin.H{
       "article": article,
   })
   return
}

 

Read Articles Handler

This handle function, returns all the articles from the database. It uses the ReadArticle() function from the database package.

func getArticles(c *gin.Context) {
   articles, err := database.ReadArticles()
   if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "err": err,
       })
       return
   }
   c.JSON(http.StatusOK, gin.H{
       "articles": articles,
   })
}

 

Update Article Handler

This function binds the incoming article data with an Article type . If the binding is successful we pass the data to the database.UpdateArticle() function.

func putArticle(c *gin.Context) {
   var article database.Article
   if err := c.ShouldBindJSON(&article); err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }
   res, err := database.UpdateArticle(&article)
   if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }
 
   c.JSON(http.StatusCreated, gin.H{
       "article": res,
   })
   return
}

 

Delete Article Handler

This handler function gets the id of the article to be deleted from the query parameter, and passes the id to the database.Delete() function.

func deleteArticle(c *gin.Context) {
   id := c.Param("id")
   err := database.DeleteArticle(id)
   if err != nil {
       c.JSON(http.StatusNotFound, gin.H{
           "message": "article not found",
       })
       return
   }
   c.JSON(http.StatusOK, gin.H{
       "message": "article deleted successfully",
   })
   return
}

 

Set up routes

The incoming requests from the client need to be mapped to their respective route handler. All these routes have been defined in an exportable function called SetupRouter(). This function initializes a Gin engine and exposes HTTP CRUD methods. It returns to the caller a Gin engine. The returned Gin engine will be used in the main.go file in the main function to spin the server.

func SetupRouter() *gin.Engine {
   r := gin.Default()
 
   r.GET("/", home)
   r.GET("/api/v1/articles/:id", getArticle)
   r.GET("/api/v1/articles", getArticles)
   r.POST("/api/v1/articles", postArticle)
   r.PUT("/api/v1/articles/:id", putArticle)
   r.DELETE("/api/v1/articles/:id", deleteArticle)
   return r
}

 

Running the server

Up to this point we have been working on the database and api packages. Now we move to the main package  in the main.go file to write code that boots the server and exposes CRUD endpoints.

The main.go file is where all the code starts running from. We will call the SetRouter() from the api package which returns the Gin engine. We then use the returned Gin engine to listen on port 5000.

main.go 

package main
 
import (
   api "example.com/go-gin-api/api"
   "example.com/go-gin-api/database"
)
 
func init() {
   database.NewPostgreSQLClient()
}
func main() {
   r := api.SetupRouter()
   r.Run(":5000")
}

 

Testing the CRUD operations

This is the moment of truth. After getting our hands dirty writing CRUD operation functions both for Gin and database , we now need to test these operations. Before the testing we need to ensure that all the dependencies have been installed by issuing the below command in the terminal.

$ go mod tidy

To run the server, issue the below command in the terminal.

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)
 
[GIN-debug] GET    /                         --> example.com/go-gin-api/api.home (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> example.com/go-gin-api/api.getArticle (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> example.com/go-gin-api/api.getArticles (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> example.com/go-gin-api/api.postArticle (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> example.com/go-gin-api/api.putArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> example.com/go-gin-api/api.deleteArticle (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.

In this section , we are going to use the CURL tool available in linux based systems to perform Create, Read, Update and Delete operations.

 

Curl Create article

$ curl http://localhost:5000/api/v1/articles \
   --include \
   --header "Content-Type: application/json" \
   --request "POST" \
   --data '{"title": "gRPC for beginners","description": "Beginners Guide","rate": 4}'

Output

{"article":{"id":18,"title":"gRPC for beginners","description":"Beginners Guide","rate":4}}

 

Curl Read Article

curl http://localhost:5000/api/v1/articles/18

Output

{"article":{"id":18,"title":"gRPC TEST for beginners and professionals","description":"gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.","rate":4}}

 

Curl Read Articles

curl http://localhost:5000/api/v1/articles

Output

{"articles":[{"id":18,"title":"gRPC TEST for beginners and professionals","description":"gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment.","rate":4}]}

 

Curl Update Article

$ curl -X PUT -H "Content-Type: application/json" -d '{"id":18,"title":"gRPC TEST for beginners and professionals", "rate":4}' http://127.0.0.1:5000/api/v1/articles/18

Output

{"article":{"id":18,"title":"gRPC TEST for beginners and professionals","description":"","rate":4}}

 

Curl Delete Article

$ curl -X DELETE http://127.0.0.1:5000/api/v1/articles/18

Output

{"message":"article deleted successfully"}

 

Summary

Go Gin framework is one of the most used web API frameworks for Go. In this article we use Go Gin to route HTTP requests to our database backend using the PostgreSQL dialect with the Go ORM (GORM). In this article we learn how to perform CRUD operations.

 

References

https://go.dev/doc/tutorial/web-service-gin
https://github.com/gin-gonic/gin

 

Categories GO

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X