Gin Example

Tôi sẽ hướng dẫn bạn xây dựng một RESTful API hoàn chỉnh bằng Gin, tích hợp GORM với PostgreSQL/MySQL, sử dụng JWT Authentication, hỗ trợ upload filepagination. Tôi sẽ giải thích từng phần chi tiết với code thực tế và best practices.


Cấu trúc Project

project/
├── cmd/
│   └── main.go
├── internal/
│   ├── config/         # Cấu hình (DB, JWT, v.v.)
│   │   └── config.go
│   ├── models/        # Định nghĩa model (GORM)
│   │   └── user.go
│   ├── repository/    # Truy cập DB
│   │   └── user_repository.go
│   ├── service/       # Logic nghiệp vụ
│   │   └── user_service.go
│   ├── handler/       # Xử lý HTTP request
│   │   └── user_handler.go
│   ├── middleware/    # Middleware (JWT, logging, v.v.)
│   │   └── auth.go
│   └── uploads/       # Thư mục lưu file upload
├── go.mod
└── go.sum

1. Thiết lập cơ bản & Kết nối GORM + PostgreSQL/MySQL

go.mod

module project

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    gorm.io/driver/mysql v1.5.1 // Hoặc gorm.io/driver/postgres
    gorm.io/gorm v1.25.5
    github.com/golang-jwt/jwt/v5 v5.2.0
)

internal/config/config.go

package config

import (
    "gorm.io/driver/mysql" // Hoặc "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type Config struct {
    DB     *gorm.DB
    JWTSecret string
}

func NewConfig() (*Config, error) {
    // Kết nối MySQL
    dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    return &Config{
        DB:        db,
        JWTSecret: "my-secret-key", // Nên lưu trong env
    }, nil
}

internal/models/user.go

package models

import "gorm.io/gorm"

type User struct {
    ID       uint   `gorm:"primaryKey" json:"id"`
    Username string `gorm:"unique" json:"username"`
    Password string `json:"-"` // Không trả về trong JSON
    Email    string `gorm:"unique" json:"email"`
}

func Migrate(db *gorm.DB) {
    db.AutoMigrate(&User{})
}

cmd/main.go

package main

import (
    "project/internal/config"
    "project/internal/handler"
    "project/internal/models"
    "project/internal/repository"
    "project/internal/service"
    "github.com/gin-gonic/gin"
)

func main() {
    cfg, err := config.NewConfig()
    if err != nil {
        panic(err)
    }

    // Migrate DB
    models.Migrate(cfg.DB)

    // Khởi tạo các tầng
    userRepo := repository.NewUserRepository(cfg.DB)
    userService := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userService, cfg.JWTSecret)

    // Khởi tạo Gin
    r := gin.Default()

    // Routes
    r.POST("/register", userHandler.Register)
    r.POST("/login", userHandler.Login)

    // Protected routes
    api := r.Group("/api", userHandler.AuthMiddleware())
    {
        api.GET("/users", userHandler.GetUsers)
        api.POST("/upload", userHandler.UploadFile)
    }

    r.Run(":8080")
}

2. Routing & Middleware với Gin

Routing

  • Gin hỗ trợ định tuyến RESTful qua các method HTTP (GET, POST, PUT, DELETE).

  • Dùng Group để tổ chức route (ví dụ: /api).

Middleware

  • Middleware xử lý logic chung (authentication, logging, v.v.).

internal/middleware/auth.go

package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

func AuthMiddleware(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            c.Abort()
            return
        }

        tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
        token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, jwt.ErrSignatureInvalid
            }
            return []byte(secret), nil
        })

        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            c.Abort()
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid claims"})
            c.Abort()
            return
        }

        c.Set("userID", claims["sub"])
        c.Next()
    }
}

3. GORM + Repository & Service

internal/repository/user_repository.go

package repository

import (
    "project/internal/models"
    "gorm.io/gorm"
)

type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *models.User) error {
    return r.db.Create(user).Error
}

func (r *UserRepository) FindByUsername(username string) (*models.User, error) {
    var user models.User
    err := r.db.Where("username = ?", username).First(&user).Error
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *UserRepository) FindAll(page, limit int) ([]models.User, int64, error) {
    var users []models.User
    var total int64
    r.db.Model(&models.User{}).Count(&total)
    err := r.db.Offset((page-1)*limit).Limit(limit).Find(&users).Error
    return users, total, err
}

internal/service/user_service.go

package service

import (
    "project/internal/models"
)

type UserRepository interface {
    Create(user *models.User) error
    FindByUsername(username string) (*models.User, error)
    FindAll(page, limit int) ([]models.User, int64, error)
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Register(username, password, email string) (*models.User, error) {
    user := &models.User{
        Username: username,
        Password: password, // Nên hash password trong thực tế
        Email:    email,
    }
    if err := s.repo.Create(user); err != nil {
        return nil, err
    }
    return user, nil
}

func (s *UserService) Login(username, password string) (*models.User, error) {
    user, err := s.repo.FindByUsername(username)
    if err != nil || user.Password != password { // So sánh password (nên dùng bcrypt)
        return nil, err
    }
    return user, nil
}

func (s *UserService) GetUsers(page, limit int) ([]models.User, int64, error) {
    return s.repo.FindAll(page, limit)
}

4. JWT Authentication

internal/handler/user_handler.go

package handler

import (
    "net/http"
    "project/internal/service"
    "strconv"
    "time"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type UserHandler struct {
    service   *service.UserService
    jwtSecret string
}

func NewUserHandler(service *service.UserService, jwtSecret string) *UserHandler {
    return &UserHandler{service: service, jwtSecret: jwtSecret}
}

func (h *UserHandler) Register(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
        Email    string `json:"email" binding:"required,email"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := h.service.Register(req.Username, req.Password, req.Email)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusCreated, user)
}

func (h *UserHandler) Login(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user, err := h.service.Login(req.Username, req.Password)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
        return
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": user.ID,
        "exp": time.Now().Add(time.Hour * 24).Unix(),
    })
    tokenStr, err := token.SignedString([]byte(h.jwtSecret))
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"token": tokenStr})
}

func (h *UserHandler) GetUsers(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))

    users, total, err := h.service.GetUsers(page, limit)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "users": users,
        "total": total,
        "page":  page,
        "limit": limit,
    })
}

func (h *UserHandler) AuthMiddleware() gin.HandlerFunc {
    return middleware.AuthMiddleware(h.jwtSecret)
}

5. Upload File

internal/handler/user_handler.go (tiếp tục)

func (h *UserHandler) UploadFile(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
        return
    }

    // Lưu file vào thư mục uploads
    dst := fmt.Sprintf("internal/uploads/%s", file.Filename)
    if err := c.SaveUploadedFile(file, dst); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save file"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "file uploaded", "path": dst})
}

Cấu hình Gin để upload:

  • Thêm r.MaxMultipartMemory = 8 << 20 (8MB) trong main.go để giới hạn kích thước file.


6. Pagination

  • Đã tích hợp trong GetUsers: dùng OffsetLimit của GORM để phân trang.

  • Trả về total để client biết tổng số bản ghi.

Ví dụ request:

GET /api/users?page=2&limit=10
Authorization: Bearer <jwt-token>

Response:

{
    "users": [...],
    "total": 50,
    "page": 2,
    "limit": 10
}

Best Practices

  • Validation: Dùng binding của Gin để validate request.

  • Security: Hash password (dùng golang.org/x/crypto/bcrypt) thay vì lưu plaintext.

  • Error Handling: Trả về mã HTTP phù hợp (400, 401, 500, v.v.).

  • Logging: Thêm middleware logging (dùng gin.Logger() hoặc custom).

  • Env: Lưu JWTSecret, DB credentials trong biến môi trường.


Tổng kết

  • Gin: Routing, middleware, và xử lý request/response.

  • GORM: Tích hợp DB với model, repository, và pagination.

  • JWT: Xác thực người dùng qua token.

  • Upload File: Lưu file với Gin.

  • Pagination: Phân trang với GORM.

Last updated