Golang Real-World (Part 2): Building the User Module (Handler, Service, Repository)
  • Golang
  • Golang Real-World (Part 2): Building the User Module (Handler, Service, Repository)

    Introduction

    In Part 1, we prepared a production-ready Golang project structure.
    Now in Part 2, we begin building our first real module:

    👉 User Module (REST API + Service Layer + Repository Layer)

    This module will include:

    • Request DTO + Validation
    • Controller (Handler)
    • Business Logic (Service)
    • Database Layer (Repository)
    • Routing
    • Standardized JSON responses

    This approach follows clean architecture and separates concerns clearly.

    1. User Table Design (MySQL)

    Here is the recommended schema:

    CREATE TABLE users (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(100),
        email VARCHAR(100) UNIQUE,
        password_hash VARCHAR(255),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );

    2. User Module Folder Structure

    internal/
    └── user/
        ├── handler.go
        ├── service.go
        ├── repository.go
        ├── dto.go
        └── model.go
    

    3. User Model

    internal/user/model.go

    package user
    
    import "time"
    
    type User struct {
        ID           int64     `json:"id"`
        Name         string    `json:"name"`
        Email        string    `json:"email"`
        PasswordHash string    `json:"-"`
        CreatedAt    time.Time `json:"created_at"`
        UpdatedAt    time.Time `json:"updated_at"`
    }

    4. DTOs (Request Objects)

    internal/user/dto.go

    package user
    
    type CreateUserDTO struct {
        Name     string `json:"name" binding:"required"`
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=6"`
    }

    5. Repository Layer

    internal/user/repository.go

    package user
    
    import (
        "database/sql"
    )
    
    type Repository interface {
        Create(user *User) (int64, error)
        FindByEmail(email string) (*User, error)
    }
    
    type repository struct {
        db *sql.DB
    }
    
    func NewRepository(db *sql.DB) Repository {
        return &repository{db}
    }
    
    func (r *repository) Create(u *User) (int64, error) {
        query := `
            INSERT INTO users (name, email, password_hash)
            VALUES (?, ?, ?)
        `
        result, err := r.db.Exec(query, u.Name, u.Email, u.PasswordHash)
        if err != nil {
            return 0, err
        }
        return result.LastInsertId()
    }
    
    func (r *repository) FindByEmail(email string) (*User, error) {
        query := `
            SELECT id, name, email, password_hash, created_at, updated_at
            FROM users WHERE email = ?
        `
        row := r.db.QueryRow(query, email)
    
        user := &User{}
        err := row.Scan(
            &user.ID, &user.Name, &user.Email, &user.PasswordHash,
            &user.CreatedAt, &user.UpdatedAt,
        )
        if err != nil {
            return nil, err
        }
    
        return user, nil
    }

    6. Service Layer (Business Logic)

    internal/user/service.go

    package user
    
    import (
        "errors"
    
        "golang.org/x/crypto/bcrypt"
    )
    
    type Service interface {
        CreateUser(dto CreateUserDTO) (*User, error)
    }
    
    type service struct {
        repo Repository
    }
    
    func NewService(repo Repository) Service {
        return &service{repo}
    }
    
    func hashPassword(raw string) (string, error) {
        bytes, err := bcrypt.GenerateFromPassword([]byte(raw), bcrypt.DefaultCost)
        return string(bytes), err
    }
    
    func (s *service) CreateUser(dto CreateUserDTO) (*User, error) {
    
        // Check if email already registered
        exists, _ := s.repo.FindByEmail(dto.Email)
        if exists != nil {
            return nil, errors.New("email is already in use")
        }
    
        hashed, err := hashPassword(dto.Password)
        if err != nil {
            return nil, err
        }
    
        user := &User{
            Name:         dto.Name,
            Email:        dto.Email,
            PasswordHash: hashed,
        }
    
        id, err := s.repo.Create(user)
        if err != nil {
            return nil, err
        }
        user.ID = id
    
        return user, nil
    }

    7. Handler Layer (HTTP Controller)

    internal/user/handler.go

    package user
    
    import (
        "github.com/gin-gonic/gin"
        "your-project/pkg/response"
    )
    
    type Handler struct {
        service Service
    }
    
    func NewHandler(service Service) *Handler {
        return &Handler{service}
    }
    
    func (h *Handler) RegisterRoutes(r *gin.Engine) {
        api := r.Group("/api/users")
        api.POST("/", h.CreateUser)
    }
    
    func (h *Handler) CreateUser(c *gin.Context) {
        var dto CreateUserDTO
    
        if err := c.ShouldBindJSON(&dto); err != nil {
            response.Error(c, err.Error())
            return
        }
    
        user, err := h.service.CreateUser(dto)
        if err != nil {
            response.Error(c, err.Error())
            return
        }
    
        response.Success(c, user)
    }
    

    8. Wiring Everything in Your Server

    internal/server/server.go

    package server
    
    import (
        "database/sql"
        "log"
    
        "github.com/gin-gonic/gin"
        _ "github.com/go-sql-driver/mysql"
    
        "your-project/internal/user"
    )
    
    func Run() {
        r := gin.Default()
    
        db, err := sql.Open("mysql", "root:password@tcp(localhost:3306)/yourdb")
        if err != nil {
            log.Fatal(err)
        }
    
        userRepo := user.NewRepository(db)
        userService := user.NewService(userRepo)
        userHandler := user.NewHandler(userService)
        userHandler.RegisterRoutes(r)
    
        r.Run(":8080")
    }

    9. Testing the API

    Send POST request:

    POST /api/users/

    Body:

    {
      "name": "Leaf",
      "email": "leaf@example.com",
      "password": "12345678"
    }

    Response:

    {
      "success": true,
      "data": {
        "id": 1,
        "name": "Long Pham",
        "email": "long@example.com",
        "created_at": "2025-11-30T00:00:00Z"
      }
    }
    

    Conclusion

    You have built a clean and scalable User module with:

    ✔ DTO validation
    ✔ Handler → Service → Repository
    ✔ MySQL integration
    ✔ Secure password hashing
    ✔ Standardized responses

    This is how real-world backend systems work in companies.

    Coming Next (Part 3)

    👉 JWT Authentication + Login API + Password Validation

    Leaf

    "Thành công nuôi dưỡng sự hoàn hảo. Sự hoàn hảo lại nuôi lớn thất bại. Chỉ có trí tưởng tượng mới tồn tại."
    4 mins