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.
- Add a book
- Delete a book
- Update a book
- Query book by ID
- 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,
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 asID
,CreatedAt
,UpdatedAt
andDeletedAt
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, 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, you already have viper initialised to handle our environment variables
We are going to do a lot more here,
- Initialise the database based on GORM
- Connect our Books router
- 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
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)
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
You don't need the intermediate type, you can just deserialize into the original type
Greate article 🙂 small advice, add
go
to your code snippets for better readability. Here how it is done.Hi @devniklesh, Great article, can you also add how to write unit & integrated tests by mocking the database?
@devniklesh why create the AddBookRequestBody type inside the
AddBook
handler? Why not directly parse the incoming JSON data into the modelctx.BindJSON(&book)