API CRUD Product data using Go, Echo, PostgreSQL, docker-compose and GORM

Nuchit Atjanawat
8 min readApr 20, 2024

--

ในบทคามนี้ เป็นการใช้ภาษา GO สร้าง API…
จัดการข้อมูลสินค้า…
โดยใช้ Echo framework …
ใช้ฐานข้อมูล PostgreSQL…ผ่าน ORM ที่ชื่อ GORM
และ รันฐานข้อมูลบน docker ผ่าน docker-compose…

สิ่งที่จำเป็น(Prerequisite)

1. GO Programming Language: https://golang.org
2. Docker destop: https://www.docker.com/products/docker-desktop/ หรือ https://rancherdesktop.io/
3. VS Code: https://code.visualstudio.com/
4. Postman: https://www.postman.com/downloads/

เราจะสร้าง API …ด้วยขั้นตอนต่อไปนี้
1. Init project
2. Download dependencies
3. Create the main file
4. Create init.sql file
5. Create docker-compose file
6. Run docker-compose
7. DBeaver connect PostgreSQL
8. Create Product Model
9. Create Response Model
10. Create .env file
11. Configuration file for database connection
12. Create Controller
13. Add Route for CRUD Controller
14. Run and Test
15. Code project on github
16. The end.

1. Init project

สร้าง Folder สำหรับเก็บ project ชื่อ: product-api เช่น D:\Bootcamp\chit\product-api จากนั้นพิมพ์ cmd ลงบน พาธดังกล่าว

เปิด Terminal

รันคำสั่ง
> go mod init product-res-api

รัน คำสั่ง = go mod init product-res-api

2. Download dependencies

> go get github.com/labstack/echo/v4
> go get -u gorm.io/gorm
> go get gorm.io/driver/postgres

3. Create the main file

สร้าง main.go รอไว้ เดี๋ยวเรากลับมาแก้ไข…

main.go

package main

import (
"net/http"

"github.com/labstack/echo/v4"
)

func main() {

// Initialize Echo instance
e := echo.New()

// Default route handler
e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, "hello world")
})


e.Logger.Fatal(e.Start(":8080"))
}

4. Create init.sql file

สร้างไฟล์ init.sql ซึ่งเราจะเขียน Script สร้างฐานข้อมูล … ตอนสั่ง รัน docker compose

init.sql

DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'janawat_db') THEN
CREATE DATABASE janawat_db;
END IF;
END $$;

5. Create docker-compose file

สร้างไฟล์ docker-compose.yml

docker-compose.yml

services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: janawat_db
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- '5432:5432'

6. Run docker-compose

รันคำสั่ง
> docker-compose up -d … สร้าง container และ start postgres
> docker-compose down … stop postgres และ remove ตัว container

7. DBeaver connect PostgreSQL

ใช้ DBeaver เชื่อมต่อฐานข้อมูล เพื่อการตรวจสอบเท่านั้น …
เพื่อดูว่า container เราสร้างได้สมบูรณ์ หรือไม่
ถ้า connect ได้ … แสดงว่า ฐานข้อมูลเราพร้อมใช้งานแล้วครับ

8. Create Product Model

เราใช้วิธี Model first … คือ สร้าง entity model ก่อน …
แล้วค่อย AutoMigrate ให้ ตัว ORM ไปสร้างตารางให้ …

product.go

package model

// Product represents a product entity in the system.
type Product struct {
ID int `gorm:"primary_key" json:"id"` // ID of the product (primary key)
Name string `json:"name"` // Name of the product
Price float32 `json:"price"` // Price of the product
}

9. Create Response Model

รูปแบบในการ Response เราอยากได้เป็น json

{
"status": xxx,
"message": "xxxx",
"data": {
"id": xxx,
"name": "xxxx",
"price": xxxx
}
}

เราจะสร้าง model เก็บข้อมูลเหล่านั้น
ดังนี้

response.go

package model

// Response represents a generic response format for API responses.
type Response struct {
Status int `json:"status"` // HTTP status code of the response
Message string `json:"message"` // Message describing the result of the operation
Data interface{} `json:"data"` // Data payload of the response
}

10. Create .env file

สร้างไฟล์ .env
เพื่อเก็บค่าคอนฟิก ของฐานข้อมูล

.env

API_PORT=1234

DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=janawat_db
DB_PORT=5432

10. Configuration file for database connection

สร้าง Folder ชื่อ config แล้ว สร้าง ไฟล์ db.go …โดยดึง ค่าคอนฟิก ฐานข้อมูลจาก .env …
การอ่านค่า .env จะใช้ dependencies: godotenv
โดยรันคำสั่ง > go get github.com/joho/godotenv เพื่อ download ลงเครื่องเราก่อน … จากนั้น เขียน Code db.go

db.go

package config

import (
"fmt"
"os"

"gorm.io/driver/postgres"
"gorm.io/gorm"

"product-res-api/model"

"github.com/joho/godotenv"
)

var database *gorm.DB
var err error

// InitDB initializes the database connection
// and performs auto migration for the Product model.
func InitDB() {
// Load environment variables from.env file
err = godotenv.Load()
if err != nil {
panic("Error loading .env file")
}

// Database connection parameters
host := os.Getenv("DB_HOST") //"localhost"
user := os.Getenv("DB_USER") // "postgres"
password := os.Getenv("DB_PASSWORD") // "postgres"
dbName := os.Getenv("DB_NAME") // "db"
port := os.Getenv("DB_PORT") // "5432"

// Construct DSN (Data Source Name) for the PostgreSQL connection
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", host, user, password, dbName, port)

// Open a connection to the PostgreSQL database
database, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("Error connecting to the database")
}

// Perform auto migration to create/update database tables based on the defined model structs
database.AutoMigrate(&model.Product{})
fmt.Println("AutoMigrate Product...")
fmt.Println("Database connected")
}

// DB returns the global database instance.
func DB() *gorm.DB {
return database
}

// ApiPort returns the port number to listen on.
func ApiPort() string {
return os.Getenv("API_PORT")
}

12. Create Controller

สร้าง folder ชื่อ controller แล้วสร้างไฟล์ controller.go

controller.go

package controller

import (
"net/http"
"product-res-api/config"
"product-res-api/model"

"github.com/labstack/echo/v4"
)

// CreateProduct creates a new product based on the request data
func GetProducts(c echo.Context) error {
// Get the database instance
db := config.DB()

// Create a slice to store products retrieved from the database
var products []model.Product

// Query the database to fetch all products
if err := db.Find(&products).Error; err != nil {
// If an error occurs during the database query, return an internal server error response

res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to retrieve products...Err:" + err.Error(),
}
return c.JSON(http.StatusInternalServerError, res)
}

// Create a response map to format the JSON response
res := model.Response{
Status: http.StatusOK,
Message: "successfully",
Data: products,
}

// Return a JSON response with the HTTP status OK (200) and the formatted response map
return c.JSON(http.StatusOK, res)

}

// GetProduct retrieves a product by its ID from the database.
func GetProduct(c echo.Context) error {
// Extract product ID from request parameters
id := c.Param("id")

// Get the database instance
db := config.DB()

// Initialize a variable to store the retrieved product
var product model.Product

// Query the database to find the product with the given ID
if err := db.First(&product, id).Error; err != nil {
// If the product is not found, return a not found response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to retrieve product id:=" + id + "...Err:" + err.Error(),
}
return c.JSON(http.StatusNotFound, res)
}

// Create a success response with the retrieved product data
res := model.Response{
Status: http.StatusOK,
Message: "successfully",
Data: product,
}

// Return a JSON response with the success response
return c.JSON(http.StatusOK, res)
}

// CreateProduct creates a new product based on the request data
func CreateProduct(c echo.Context) error {
// Create a new product instance
product := new(model.Product)

// Bind the request body to the product struct
if err := c.Bind(product); err != nil {
// If there's an error in binding the request body, return a bad request response
res := model.Response{
Status: http.StatusBadRequest,
Message: "Failed to create product...Err:" + err.Error(),
}
return c.JSON(http.StatusBadRequest, res)
}

// Get the database instance
db := config.DB()

// Create the product in the database
if err := db.Create(product).Error; err != nil {
// If there's an error creating the product, return an internal server error response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to create product...Err:" + err.Error(),
}
return c.JSON(http.StatusInternalServerError, res)
}

// If creation is successful, return a success response with the created product data
res := model.Response{
Status: http.StatusCreated,
Message: "successfully",
Data: product,
}
return c.JSON(http.StatusCreated, res)

}

// UpdateProduct updates a product by its ID from the database.
func UpdateProduct(c echo.Context) error {

// Extract product ID from request parameters
id := c.Param("id")

// Create a new product instance to hold updated data
product := new(model.Product)

// Bind the request body to the product struct
if err := c.Bind(product); err != nil {
// If there's an error in binding the request body, return a bad request response
res := model.Response{
Status: http.StatusBadRequest,
Message: "Failed to update product...Err:" + err.Error(),
}

return c.JSON(http.StatusBadRequest, res)
}

// Check if the product exists in the database
existingProduct := model.Product{}
if err := config.DB().First(&existingProduct, id).Error; err != nil {
// If the product doesn't exist, return a not found response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to update product...Err:" + err.Error(),
}
return c.JSON(http.StatusNotFound, res)
}

// Update the product in the database
if err := config.DB().Model(&model.Product{}).Where("id =?", id).Updates(product).Error; err != nil {
// If there's an error updating the product, return an internal server error response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to update product...Err:" + err.Error(),
}
return c.JSON(http.StatusInternalServerError, res)

}

// Read the updated product from the database
updatedProduct := model.Product{}
if err := config.DB().First(&updatedProduct, id).Error; err != nil {
// If there's an error reading the updated product, return an internal server error response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to update product...Err:" + err.Error(),
}

return c.JSON(http.StatusInternalServerError, res)
}

// Create a success response with the updated product data
res := model.Response{
Status: http.StatusOK,
Message: "successfully",
Data: updatedProduct,
}

// Return a JSON response with the success response
return c.JSON(http.StatusOK, res)
}

// DeleteProduct deletes a product by its ID from the database.
func DeleteProduct(c echo.Context) error {
// Extract product ID from request parameters
id := c.Param("id")

// Get the database instance
db := config.DB()

// Check if the product exists in the database
existingProduct := model.Product{}
if err := config.DB().First(&existingProduct, id).Error; err != nil {
// If the product doesn't exist, return a not found response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to delete product...Err:" + err.Error(),
}
return c.JSON(http.StatusNotFound, res)
}

// Delete the product from the database
if err := db.Delete(&model.Product{}, id).Error; err != nil {
// If there's an error deleting the product, return an internal server error response
res := model.Response{
Status: http.StatusNotFound,
Message: "Failed to delete product...Err:" + err.Error(),
}
return c.JSON(http.StatusInternalServerError, res)
}

// If deletion is successful, return a success response
res := model.Response{
Status: http.StatusOK,
Message: "Delete product successfully",
Data: nil,// No data to return after deletion
}

// Return a JSON response with the success response
return c.JSON(http.StatusOK, res)
}

13. Add Route for CRUD Controller

เปิดไฟล์ main.go แล้ว เขียน Code เพิ่มลงไป …

Code ในส่วน Router…

...
// Group routes related to product endpoints
productRoute := e.Group("/product")
productRoute.GET("", controller.GetProducts) // Get all products
productRoute.GET("/:id", controller.GetProduct) // Get a specific product by ID

productRoute.POST("", controller.CreateProduct) // Create a new product
productRoute.DELETE("/:id", controller.DeleteProduct) // Delete a product by ID
productRoute.PUT("/:id", controller.UpdateProduct) // Update a product by ID
...

Code สมบูรณ์

main.go

package main

import (
"net/http"

"product-res-api/config"
"product-res-api/controller"

"github.com/labstack/echo/v4"
)

func main() {

// Initialize Echo instance
e := echo.New()

// Default route handler
e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, "hello world")
})

// Connect to the database
config.InitDB()
gorm := config.DB()

// Retrieve the underlying *sql.DB instance from GORM
dbGorm, err := gorm.DB()
if err != nil {
panic(err)
}

// Ping the database to check connectivity
dbGorm.Ping()

// Group routes related to product endpoints
productRoute := e.Group("/product")
productRoute.GET("", controller.GetProducts) // Get all products
productRoute.GET("/:id", controller.GetProduct) // Get a specific product by ID

productRoute.POST("", controller.CreateProduct) // Create a new product
productRoute.DELETE("/:id", controller.DeleteProduct) // Delete a product by ID
productRoute.PUT("/:id", controller.UpdateProduct) // Update a product by ID

// Start the server on port 8080
apiPort:=config.ApiPort();
// e.Logger.Fatal(e.Start(":8080"))
e.Logger.Fatal(e.Start(":" + apiPort))
}

14. Run and Test

รันคำสั่ง > go run main.go

ทดสอบยิง API ผ่าน Postman

15. Code project on github

https://github.com/nuchit2019/go-crud-product-api

16. The end.

--

--