DEV Community

Dev Niklesh
Dev Niklesh

Posted on

CRUD api with Go Gin framework (production ready)

This is my second post on GO lang with Gin framework and PostgreSQL. Read my first post which takes you just 2 mins to setup the project and read env variables using viper!.

In this post, we are creating Library CRUD APIs to read data from PostgreSQL Database.

  1. Add a book
  2. Delete a book
  3. Update a book
  4. Query book by ID
  5. Query all books

Project Structure

Run these commands to create the necessary files needed to create the APIs.

Note that the project structure can be much simpler than this, but my intention is to create a production ready structure that is easy to maintain and extend the code base in future.

Let's create our folders,

mkdir -p cmd pkg/books pkg/common/db  pkg/common/models
Enter fullscreen mode Exit fullscreen mode

Let's add some files,

touch pkg/books/add_book.go pkg/books/controller.go pkg/books/delete_book.go pkg/books/get_book.go pkg/books/get_books.go pkg/books/update_book.go pkg/common/db/db.go  pkg/common/models/book.go
Enter fullscreen mode Exit fullscreen mode

Final project structure should look something like this,
CRUD Project structure


Package Installations

Let's install the packages required,

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres
Enter fullscreen mode Exit fullscreen mode

Book Model

Here let's create the Book model.

Let's add code to pkg/common/models/book.go

package models

import "gorm.io/gorm"

type Book struct {
    gorm.Model
    Title       string `json:"title"`
    Author      string `json:"author"`
    Description string `json:"description"`
}

Enter fullscreen mode Exit fullscreen mode

gorm.model will add properties such as ID, CreatedAt, UpdatedAt and DeletedAt for us out of the box.


Database Initialization

Now that the Book Model is done, we need to configre GORM and auto migrate the schema we just created. This AutoMigrate function will create the books table in the database for us as soon as we run this application.

Let' add code to /pkg/common/db/db.go

package db

import (
    "log"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func Init(url string) *gorm.DB {
    db, err := gorm.Open(postgres.Open(url), &gorm.Config{})

    if err != nil {
        log.Fatalln(err)
    }

    db.AutoMigrate(&models.Book{})

    return db
}

Enter fullscreen mode Exit fullscreen mode

Book Handlers

Let's add some handlers to our Book API.

Controller

The book handlers/route will be based on so-called Pointer receivers, for that, we define its struct.

This struct will receive the database information later, so whenever we call a book handler/route, we will have access to GROM.

We're going to change this file once later again.

Let's add code to pkg/books/controller.go


package books

import (
    "gorm.io/gorm"
)

type handler struct {
    DB *gorm.DB
}
Enter fullscreen mode Exit fullscreen mode

Add Book Handler

Here, We get the request body, declare a new book variable, merge the request body with this book variable, and create a new database entry. Then, we create a response with the book information.

After the imports, we defile a struct for the request's body. You can see the pointer receiver (h handler) being used in the AddBook function. We are using h (handler) to access the database.

Let's add the code to pkg/books/add_book.go

package books

import (
    "net/http"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "github.com/gin-gonic/gin"
)

type AddBookRequestBody struct {
    Title       string `json:"title"`
    Author      string `json:"author"`
    Description string `json:"description"`
}

func (h handler) AddBook(ctx *gin.Context) {
    body := AddBookRequestBody{}

    if err := ctx.BindJSON(&body); err != nil {
        ctx.AbortWithError(http.StatusBadRequest, err)
        return
    }

    var book models.Book

    book.Title = body.Title
    book.Author = body.Author
    book.Description = body.Description

    if result := h.DB.Create(&book); result.Error != nil {
        ctx.AbortWithError(http.StatusNotFound, result.Error)
        return
    }

    ctx.JSON(http.StatusCreated, &book)
}

Enter fullscreen mode Exit fullscreen mode

Get Books Handler

On this route, we going to return all books from our database. This works now pretty fast, but as soon as you have bigger data to handle, better go for a pagination approach.

Let’s add code to pkg/books/get_books.go

package books

import (
    "net/http"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "github.com/gin-gonic/gin"
)

func (h handler) GetBooks(ctx *gin.Context) {
    var books []models.Book

    if result := h.DB.Find(&books); result.Error != nil {
        ctx.AbortWithError(http.StatusNotFound, result.Error)
        return
    }

    ctx.JSON(http.StatusOK, &books)
}

Enter fullscreen mode Exit fullscreen mode

Get Book Handler

Here, we just respond with only 1 book based on the ID which we get from a parameter.

Let’s add code to pkg/books/get_book.go

package books

import (
    "net/http"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "github.com/gin-gonic/gin"
)

func (h handler) GetBook(ctx *gin.Context) {
    id := ctx.Param("id")

    var book models.Book

    if result := h.DB.First(&book, id); result.Error != nil {
        ctx.AbortWithError(http.StatusNotFound, result.Error)
        return
    }

    ctx.JSON(http.StatusOK, &book)
}

Enter fullscreen mode Exit fullscreen mode

Update Book Handler

If we add a book, we also should have the option to update created books. This route is similar to the AddBook route we have coded earlier.

Let’s add code to pkg/books/update_book.go

package books

import (
    "net/http"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "github.com/gin-gonic/gin"
)

type UpdateBookRequestBody struct {
    Title       string `json:"title"`
    Author      string `json:"author"`
    Description string `json:"description"`
}

func (h handler) UpdateBook(ctx *gin.Context) {
    id := ctx.Param("id")
    body := UpdateBookRequestBody{}

    if err := ctx.BindJSON(&body); err != nil {
        ctx.AbortWithError(http.StatusBadRequest, err)
        return
    }

    var book models.Book

    if result := h.DB.First(&book, id); result.Error != nil {
        ctx.AbortWithError(http.StatusNotFound, result.Error)
        return
    }

    book.Title = body.Title
    book.Author = body.Author
    book.Description = body.Description

    h.DB.Save(&book)

    ctx.JSON(http.StatusOK, &book)
}

Enter fullscreen mode Exit fullscreen mode

Delete Book Handler

This is our last route in this article. Here, we delete a book based on its ID, but only, if the desired entry exists inside the database. We only respond with an HTTP status code.

Let’s add code to pkg/books/delete_book.go

package books

import (
    "net/http"

    "github.com/<YOUR-USERNAME>/go-gin-postgresql-api/pkg/common/models"
    "github.com/gin-gonic/gin"
)

func (h handler) DeleteBook(ctx *gin.Context) {
    id := ctx.Param("id")

    var book models.Book

    if result := h.DB.First(&book, id); result.Error != nil {
        ctx.AbortWithError(http.StatusNotFound, result.Error)
        return
    }

    h.DB.Delete(&book)

    ctx.Status(http.StatusOK)
}

Enter fullscreen mode Exit fullscreen mode

Controller (again)

The routes are done. Now we need to modify the controller file once again. This time, we create a function called RegisterRoutes, it’s pretty self-explaining what it does, right?

Do you remember the receiver pointer? Here we get the receiver pointer for our routes/handlers.

Update controller code to this,

package books

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

type handler struct {
    DB *gorm.DB
}

func RegisterRoutes(router *gin.Engine, db *gorm.DB) {
    h := &handler{
        DB: db,
    }

    routes := router.Group("/books")
    routes.POST("/", h.AddBook)
    routes.GET("/", h.GetBooks)
    routes.GET("/:id", h.GetBook)
    routes.PUT("/:id", h.UpdateBook)
    routes.DELETE("/:id", h.DeleteBook)
}

Enter fullscreen mode Exit fullscreen mode

Main File

If you are following from previous tutorial, you already have viper initialised to handle our environment variables

We are going to do a lot more here,

  1. Initialise the database based on GORM
  2. Connect our Books router
  3. Starting the application

We will also make changes to this file further into the tutorial.

Let's add the code to /cmd/main.go

package main

import (
    "github.com/DevNiklesh/go-gin-postgresql-api/pkg/books"
    "github.com/DevNiklesh/go-gin-postgresql-api/pkg/common/db"
    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./pkg/common/envs/.env")
    viper.ReadInConfig()

    port := viper.Get("PORT").(string)
    dbUrl := viper.Get("DB_URL").(string)

    router := gin.Default()
    dbHandler := db.Init(dbUrl)

    books.RegisterRoutes(router, dbHandler)

    router.GET("/", func(ctx *gin.Context) {
        ctx.JSON(200, gin.H{
            "port":  port,
            "dbUrl": dbUrl,
        })
    })

    router.Run(port)
}

Enter fullscreen mode Exit fullscreen mode

Let's run the code now,

make server

(OR)

go run cmd/main
Enter fullscreen mode Exit fullscreen mode

Running the code, console output

Test out the APIs now!

Now you can use this for all your micro-services and its much easier to maintain.

In the next tutorial, we are going to see how to run CRON jobs using Golang. So make sure to follow me here as well as in linkedIn to get notified about it.

Please like and share, if you enjoy this series. Much Appreciated! Thanks.


Top comments (5)

Collapse
 
0xthomas3000 profile image
Thomas N.

Can we just parse the JSON request data into the original model in order to add a new Book without creating another struct like AddBookRequestBody?
I can parse it directly but not too sure if there's any security or performance issue for doing this? Thanks

Collapse
 
aevitas profile image
aevitas

You don't need the intermediate type, you can just deserialize into the original type

Collapse
 
dinifarb profile image
DiniFarb

Greate article 🙂 small advice, add go to your code snippets for better readability. Here how it is done.

Collapse
 
nidhey27 profile image
Nidhey Nitin Indurkar

Hi @devniklesh, Great article, can you also add how to write unit & integrated tests by mocking the database?

Collapse
 
the_heedo profile image
Raj Abishek

@devniklesh why create the AddBookRequestBody type inside the AddBook handler? Why not directly parse the incoming JSON data into the model ctx.BindJSON(&book)