Building a REST API with GO, Gin framework, and GORM.

I was looking for a framework to start a backend project to learn and use GORM, which is an ORM library for Go. And Gin caught my attention, I found the documentation really good with examples that help to build a REST API. I wrote this article as a part of my learning process, so I will share with you how to build a REST API using Gin HTTP Web Framework and GORM as ORM.

Gin Framework

Acording to its documentation:

Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.

Prerequisites:

  • GO installed

  • Gin framework

  • GORM

restApi/
           grocery/
              groceryService.go
          model/
              groceryModel.go
              database.go
          main.go
          go.mod
          go.sum
          database.db

We will start defining our model in grocery.go

groceryModel.go

package model

import (

    "gorm.io/gorm"
)

type Grocery struct {
    gorm.Model
    Name      string `json:"name"`
    Quantity  int    `json:"quantity"`

}

Declaring gorm.Model above model's fields will add these fields to the model automatically when you add data to the database:

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

database.go


package model

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)


func Database() (*gorm.DB, error) {

    db, err := gorm.Open(sqlite.Open("./database.db"), &gorm.Config{})

    if err != nil {
        log.Fatal(err.Error())
    }

    if err = db.AutoMigrate(&Grocery{}); err != nil {
        log.Println(err)
    }


    return db, err

}

The database function will connect the database. I'm using SQLite, but GORM has drivers for MySQL, PostgreSQL, and SQLServer. The db variable will open the database.db file where the data will be stored.

db.AutoMigrate(&Grocery{}) will automatically migrate our schema, and keep it up to date.

groceryService.go

package grocery

import (
    "net/http"
    "log"

    "github.com/carlosm27/apiwithgorm/model"
    "github.com/gin-gonic/gin"
)

type NewGrocery struct {
    Name     string `json:"name" binding:"required"`
    Quantity int    `json:"quantity" binding:"required"`
}

type GroceryUpdate struct {
    Name     string `json:"name"`
    Quantity int    `json:"quantity"`
}

func GetGroceries(c *gin.Context) {

    var groceries []model.Grocery

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }

    if err := db.Find(&groceries).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, groceries)

}

GetGroceries is the function that will get all the groceries stored in the database. We declared the variable "groceries" and then we use the variable db to use the method Find, associated with *gorm.DB and pass the address of groceries as an argument. Then we use "c.JSON" to send the status and the data in groceries as JSON as a response.

func GetGrocery(c *gin.Context) {

    var grocery model.Grocery

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }

    if err := db.Where("id= ?", c.Param("id")).First(&grocery).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Grocery not found"})
        return
    }

    c.JSON(http.StatusOK, grocery)

}

GetGrocery gets you a grocery by its id. It takes the id from the URI. We use the method Where, to get the grocery by its id. If an id doesn't exist in the database, a Not Found status and an error message will be sent as a response. If it exists, then the grocery data will be sent as a response.

func PostGrocery(c *gin.Context) {

    var grocery NewGrocery

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

    newGrocery := model.Grocery{Name: grocery.Name, Quantity: grocery.Quantity}

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }

    if err := db.Create(&newGrocery).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, newGrocery)
}

We make a struct, NewGrocery, with the fields Name and Quantity and we add "binding: required". And then in PostGrocery, we declare a variable grocery with NewGrocery as the type. If one of the fields is missing or with a typo in the post request, a Bad Request status with an error will be sent as a response.

If everything is correct, like this: {"name": "Apple", "quantity": 5}. Another variable will be created, newGrocery, with Grocery as the type. And its data will be the same as the grocery variable. Then we call the method Create, and pass the address of newGrocery. And the status and the data will be sent as a response.

func UpdateGrocery(c *gin.Context) {

    var grocery model.Grocery

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }

    if err := db.Where("id = ?", c.Param("id")).First(&grocery).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Grocery not found!"})
        return
    }

    var updateGrocery GroceryUpdate

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

    if err := db.Model(&grocery).Updates(model.Grocery{Name: updateGrocery.Name, Quantity: updateGrocery.Quantity}).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, grocery)


}

In UpdateGrocery, first, we check if the id passes it in the URI does exist, if it does not, an error and Not Found status will be sent. If it does exist, a variable with GroceryUpdate as the type will be created to check the fields, if everything is correct, then we use the method Updates, and send the status with the grocery updated as a response.

func DeleteGrocery(c *gin.Context) {

    var grocery model.Grocery

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }

    if err := db.Where("id = ?", c.Param("id")).First(&grocery).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Grocery not found!"})
        return
    }

    if err := db.Delete(&grocery).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "Grocery deleted"})

}

In DeleteGrocery if the id doesn't exist in the database, an error and a Not Found status will be sent. If it does exist, the method Delete deletes the grocery. Status and a message will be sent as a response.

main.go

package main

import (
    "log"

    "github.com/carlosm27/apiwithgorm/grocery"
    "github.com/carlosm27/apiwithgorm/model"
    "github.com/gin-gonic/gin"
)

func main() {

    db, err := model.Database()
    if err != nil {
        log.Println(err)
    }
    db.DB()

    router := gin.Default()

    router.GET("/groceries", grocery.GetGroceries)
    router.GET("/grocery/:id", grocery.GetGrocery)
    router.POST("/grocery", grocery.PostGrocery)
    router.PUT("/grocery/:id", grocery.UpdateGrocery)
    router.DELETE("/grocery/:id", grocery.DeleteGrocery)

    log.Fatal(router.Run(":10000"))
}

In the main function, we called the model.Database() to initialize the database.

Then we start up our router with:

router := gin.Default()

Now we can use the respective HTTP methods with their URI and functions as arguments. The pattern to use the methods GET, POST, PUT and DELETE is the same:

        //method.(URI, function)   
router.GET("/groceries", grocery.GetGroceries)

Then we use the method Run and pass the port as an argument. We can use Run without an argument, by default it serves on :8080.

Thank you for taking your time and read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, anything; please leave a comment or contact me through Twitter, LinkedIn.

The complete code is here

References: