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

Golang Real-World (Part 2): Building the User Module (Handler, Service, Repository)
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

About Leaf 49 Articles
"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."