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 file và pagination . 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
Copy 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
Copy 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
Copy 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
Copy 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
Copy 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
Copy 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
Copy 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
Copy 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
Copy 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)
Copy 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.
Đã tích hợp trong GetUsers
: dùng Offset
và Limit
của GORM để phân trang.
Trả về total
để client biết tổng số bản ghi.
Ví dụ request:
Copy GET /api/users?page=2&limit=10
Authorization: Bearer <jwt-token>
Response:
Copy {
"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.