API CRUD Product data using Go, Echo, PostgreSQL, docker-compose and GORM
ในบทคามนี้ เป็นการใช้ภาษา 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
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