CRUD api with Go Gin framework (production ready)
Tags: go, crud, tutorial, programming
Published: 2022-06-18 09:56:47

> _This is my second post on GO lang with Gin framework and PostgreSQL. Read my [first post](https://dev.to/devniklesh/how-to-read-env-variables-in-golang-using-viper-2jd1) 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
```

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
```
Final project structure should look something like this,
![CRUD Project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kq6fk7yva4quyczab8i3.png)

---

## 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
```

---

## 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"`
}

```

> _`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
}

```

---
## Book Handlers
Let's add some handlers to our Book API.


### Controller 
The book handlers/route will be based on so-called [Pointer receivers](https://go.dev/tour/methods/4), 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
}
```
---
### 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)
}

```

---
### 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)
}

```
---
### 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)
}

```
---
### 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)
}

```
---
### 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)
}

```
---
## 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)
}

```
---

## Main File

> _If you are following from [previous tutorial](https://dev.to/devniklesh/how-to-read-env-variables-in-golang-using-viper-2jd1), 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)
}

```

Let's run the code now,
```
make server

(OR)

go run cmd/main
```
![Running the code, console output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/worye8wcwdomhgqc9i0i.png)

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](https://www.linkedin.com/in/devniklesh/) to get notified about it. 

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


---