
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
