Creating REST APIs with Go and Database

Photo by Dušan veverkolog on Unsplash
Photo by Dušan veverkolog on Unsplash
Go has simplified syntax and great run-time efficiency. This article will explain how to create a backend application with Go, implementing REST APIs and connecting to the database.

Go is a very popular programming language in recent years. It has simplified syntax and great run-time efficiency. Nowadays, Go has been used extensively to develop backend applications. This article will explain how to create a backend application with Go, implementing REST APIs and connecting to the database.

The complete code for this chapter can be found in .

Creating a Go Project

First, we use the following commands to create a Go project called GoRestDbExample. The package name is github.com/xhhuango/waynestalk/gorestdbexample.

% mkdir GoRestDbExample
% cd GoRestDbExample
GoRestDbExample % go mod init github.com/xhhuango/waynestalk/gorestdbexample

Create user.go and declare User struct. json is a struct field tag, specifying the name of this field in JSON when encoding struct into JSON. For example, we want Username field to be username in JSON instead of capital U. In addition, “-” means to ignore this field.

package main

type User struct {
    Username string `json:"username"`
    Password string `json:"-"`
    Name     string `json:"name"`
    Age      uint   `json:"age"`
}

Create main.go and add main() to print a user.

package main

import "fmt"

func main() {
    fmt.Printf("%v", User{
        Username: "bruce@gmail.com",
        Password: "123456",
        Name: "Bruce",
        Age: 24,
    })
}

Use the following command to execute this project.

GoRestDbExample % go run github.com/xhhuango/waynestalk/gorestdbexample
{bruce@gmail.com 123456 Bruce 24}

Building a RESTful Web Service with Gin

Go’s built-in net/http package can implement REST APIs. However, many third-party packages also provide routers. In this article, we will use gin-gonic/gin to build a RESTful Web Service. Use the following instructions to install Gin.

GoRestDbExample % go get -u github.com/gin-gonic/gin

Gin can build a RESTful Web Service in just 2 or 3 lines. Modified main.go as below to build a web service on port 7788, but it has not yet implemented any REST APIs.

package main

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

func main() {
    r := gin.Default()

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

HTTP Requests

Create handler.go, and its code is as follows. JSON() converts user slice into JSON and outputs it to the response body.

package main

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

var users = []User{
    {Username: "bruce@gmail.com", Password: "123", Name: "Bruce", Age: 24},
    {Username: "monika@gmail.com", Password: "123", Name: "Monika", Age: 18},
}

func ListUsers(c *gin.Context) {
    c.JSON(http.StatusOK, users)
}

Then, add ListUsers() to the router in main.go. We call GET() to indicate that this is a GET Request and the path is /users. Gin also provides POST(), PUT(), DELETE(), etc.

package main

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

func main() {
    r := gin.Default()
    r.GET("/users", ListUsers)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

After executing the project, use curl command to send an HTTP GET Request.

% curl --location --request GET 'localhost:7788/users'
[{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18}]

Path Variables

Obtaining path variables with Gin is very simple. In handler.go, add GetUser() as below. We can use ShouldBindUri() to get path variables. param.Name use field tag uri to specify the binding path name.

func GetUser(c *gin.Context) {
    var param struct {
        Name string `uri:"name"`
    }

    if err := c.ShouldBindUri(&param); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    for _, u := range users {
        if u.Name == param.Name {
            c.JSON(http.StatusOK, u)
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "error": fmt.Sprintf("user %s not found", param.Name),
    })
}

Remember to add GetUser() to router in main.go. When setting the router, the path variable will have a semicolon prefix.

package main

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

func main() {
    r := gin.Default()
    r.GET("/users", ListUsers)
    r.GET("/users/:name", GetUser)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

Query Parameters

The way to obtain query parameters is similar to that of path variables.

Change ListUsers() to the following. In this code, we need to obtain query parameter age. Different from path variables with uri, query parameters uses field tag form to specify the binding name, and call ShouldBindQuery() to get query parameters.

func ListUsers(c *gin.Context) {
    var query struct {
        Age uint `form:"age"`
    }

    if err := c.ShouldBindQuery(&query); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    for _, u := range users {
        if u.Age == query.Age {
            c.JSON(http.StatusOK, u)
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{
        "error": fmt.Sprintf("user with age %d not found", query.Age),
    })
}
% curl --location --request GET 'localhost:7788/users?age=18'
{"username":"monika@gmail.com","name":"Monika","age":18}

Request Body

The last one is response body passed by POST Requests. In handler.go, we add CreateUser() to add users. Gin uses field tag json to specify the binding name, and then uses ShoulddBindJSON() to get the data.

func CreateUser(c *gin.Context) {
    var req struct {
        Username string `json:"username"`
        Password string `json:"password"`
        Name     string `json:"name"`
        Age      uint   `json:"age"`
    }

    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    users = append(users, User{
        Username: req.Username,
        Password: req.Password,
        Name:     req.Name,
        Age:      req.Age,
    })

    c.JSON(http.StatusOK, users)
}

Add CreateUser() to the router.

package main

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

func main() {
    r := gin.Default()
    r.GET("/users", ListUsers)
    r.GET("/users/:name", GetUser)
    r.POST("/users", CreateUser)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

Use curl to test POST /users.

% curl --location --request POST 'localhost:7788/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username": "peter@gmail.com",
    "password": "123",
    "name": "Peter",
    "age": 30
}'
[{"username":"bruce@gmail.com","name":"Bruce","age":24},{"username":"monika@gmail.com","name":"Monika","age":18},{"username":"peter@gmail.com","name":"Peter","age":30}]

Validation

Gin also provides binding verification. You can validate whether the data exists or whether the format is correct when binding. It uses struct tag binding to specify binding rules. For example, if you want to specify that a certain field must be provided, you can add binding:"required"as follows.

func GetUser(c *gin.Context) {
    var param struct {
        Name string `uri:"name" binding:"required"`
    }
    ...
}

In addition, you can also specify the data format, such as email, or the data must be greater than a certain value.

func CreateUser(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required,email"`
        Password string `json:"password" binding:"required"`
        Name     string `json:"name" binding:"required"`
        Age      uint   `json:"age" binding:"required,min=10"`
    }
    ...
}

The above is just a list of some binding settings. In fact, there are many more settings. However, these settings can not be not found on Gin’s official website, because Gin uses go-playground/validator package to implement validation. Therefore, we can check the list of available settings on validator’s website.

Connecting Databases with GORM

Most of backend applications also connect to databases. Let’s connect a database for our RESTful web service. Go has many third-party database packages. In this article, we use go-gorm/gorm to connect a database. GORM is an ORM library used by quite a few people. In addition, according to the database you use, you have to install the corresponding driver.

Before connecting to a database, let’s modify User first. Add gorm.Model to User.

package main

import "gorm.io/gorm"

type User struct {
    gorm.Model
    Username string `json:"username"`
    Password string `json:"-"`
    Name     string `json:"name"`
    Age      uint   `json:"age"`
}

gorm.Model contains 4 fields, where ID is tagged as a primary key.

package gorm

import "time"

type Model struct {
	ID        uint      `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

You can also define primary key field by yourself without using gorm.Model, as follows.

package main

import "gorm.io/gorm"

type User struct {
    UserID   uint64 `json:"userId" gorm:"primarykey"`
    Username string `json:"username"`
    Password string `json:"-"`
    Name     string `json:"name"`
    Age      uint   `json:"age"`
}

SQLite

SQLite is a simple but very convenient database. Use the following commands to install GORM and SQLite driver.

GoRestDbExample % go get -u gorm.io/gorm
GoRestDbExample % go get -u gorm.io/driver/sqlite

Modify main.go as follows. Call gorm.Open() to open ./test.db database. If the file does not exist, it will be created automatically. After that, call AutoMigrate() to create a table based on User.

package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

var db *gorm.DB

func main() {
    _db, err := gorm.Open(sqlite.Open("./test.db"), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db = _db

    if err := db.AutoMigrate(&User{}); err != nil {
        panic(err)
    }

    r := gin.Default()
    r.GET("/users", ListUsers)
    r.GET("/users/:name", GetUser)
    r.POST("/users", CreateUser)

    if err := r.Run("localhost:7788"); err != nil {
        panic(err)
    }
}

In addition, SQLite also supports In-Memory database. It does not create files, but creates a temporary database in memory. In the development stage, it is quite convenient when the table columns are still uncertain. In addition, it is also very useful when writing test programs. To use In-Memory database, you just need to change the file name to :memory:.

func main() {
    _db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db = _db

    if err := db.AutoMigrate(&User{}); err != nil {
        panic(err)
    }

    ...
}

Query

Querying with GORM is quite simple. Create repository.go and paste the following code. Call Where() to set the WHERE conditions, and then call Find() to query data.

func findUsers(age uint) ([]User, error) {
    var users []User
    if result := db.Where("Age > ?", age).Find(&users); result.Error != nil {
        return nil, result.Error
    }
    return users, nil
}

In addition to using Where() to set conditions, GORM also supports using struct to set conditions, as follows.

func findByName(name string) (*User, error) {
    var user User
    if result := db.Where(&User{Name: name}).First(&user); result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

Insertion

It is also very simple to insert data with GORM. Create a User, fill in all the data to be added, and then call Create() to insert it. If the insertion is successful, the primary key field in the User will automatically be assigned the primary key.

func insertUser(user *User) error {
    if result := db.Create(user); result.Error != nil {
        return result.Error
    }
    return nil
}

Conclusion

This article is just a rough introduction to Gin and GORM, they have far more features than these. If you want to understand them in depth, you must read their documents yourself. Fortunately, their documents are very complete, and there are many related resources on the Internet.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like