When building APIs in Go, it’s easy to start with a simple structure—routes, handlers, and database calls all mixed together. But as your project grows, this “quick start” approach can lead to messy, tightly coupled code that is hard to test, scale, and maintain.
This is where Clean Architecture comes in. Popularized by Robert C. Martin (Uncle Bob), Clean Architecture emphasizes separation of concerns and independence from frameworks, databases, or external services. The goal is to keep your business logic at the core, while everything else—such as web frameworks, databases, or third-party APIs—remains replaceable.
In this tutorial, you’ll learn how to build a scalable REST API in Go using Clean Architecture principles. We’ll structure the project into layers for Entities, Use Cases, Interfaces (Repositories), and Infrastructure, and we’ll use the Gin framework for HTTP handling. By the end, you’ll have a well-structured, testable, and maintainable Go API that you can easily extend as your application grows.
Prerequisites
Before starting, make sure you have the following installed and set up:
-
Go 1.23+ is installed on your system
-
Basic knowledge of Go modules, structs, and interfaces
-
Familiarity with building simple REST APIs in Go
-
A running PostgreSQL or MySQL database (for the persistence layer)
-
A tool to test APIs, such as Postman or
curl
We’ll keep the example simple (a basic User API), but the structure can be applied to larger and more complex applications.
Project Setup
The first step is to set up a clean and organized project structure that reflects the principles of Clean Architecture.
1. Initialize Go Module
In your project directory, initialize a new Go module:
mkdir go-clean-architecture
cd go-clean-architecture
go mod init github.com/yourusername/go-clean-architecture
This creates a go.mod
file, which will manage dependencies for your project.
2. Project Structure
We’ll organize the project into layers:
go-clean-architecture/
│
├── cmd/ # Application entry point
│ └── main.go
│
├── internal/ # Internal application code
│ ├── entity/ # Core business models (Entities)
│ ├── usecase/ # Application business logic (Use Cases)
│ ├── repository/ # Repository interfaces (contracts)
│ ├── infrastructure/ # Implementations (DB, external services)
│ └── delivery/ # Delivery layer (HTTP handlers/controllers)
│
└── go.mod
Why this structure?
-
entity
→ Represents core business objects (e.g., User). -
usecase
→ Contains application logic (e.g., CreateUser, GetUser). -
repository
→ Interfaces defining how the app interacts with persistence/storage. -
infrastructure
→ Actual implementations of repositories (e.g., PostgreSQL). -
delivery
→ Handles API requests/responses, using frameworks like Gin. -
cmd
→ Entry point to run the application.
3. Install Dependencies
We’ll use Gin for HTTP handling and GORM for database interactions (you could also use database/sql
if you prefer). Install them with:
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/postgres
✅ At this point, your project is initialized and structured to follow Clean Architecture principles.
Define Entities (Core Business Models)
In Clean Architecture, Entities are the core business objects of your application. They should be framework-agnostic and contain only the essential fields and business rules.
For this tutorial, we’ll build a simple User API, so our main entity will be User
.
1. Create the entity
package
Inside your project, create the folder:
mkdir -p internal/entity
Then create a new file user.go
:
package entity
import "time"
// User represents the core business model for our app.
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Key Points
-
The
User
struct is independent of any framework (Gin, GORM, etc.). -
We only define the business fields needed for the domain.
-
If later we add other entities (e.g.,
Product
,Order
), they will live in this sameentity
package.
This entity
layer is the innermost circle of Clean Architecture — it should not depend on anything external.
✅ Now we have our core entity defined.
Define Use Cases (Business Logic)
In Clean Architecture, Use Cases represent the application-specific business rules. They orchestrate the flow of data between the entities and the repositories, without depending on external frameworks.
For our User
entity, we’ll define basic use cases:
-
CreateUser
-
GetUserByID
-
ListUsers
1. Create the usecase
package
Inside your project, create the folder:
mkdir -p internal/usecase
Then create a new file user_usecase.go
:
package usecase
import (
"github.com/yourusername/go-clean-architecture/internal/entity"
)
// UserRepository defines the expected behavior from any User storage.
type UserRepository interface {
Create(user *entity.User) error
GetByID(id int64) (*entity.User, error)
GetAll() ([]entity.User, error)
}
// UserUsecase contains the business logic for users.
type UserUsecase struct {
repo UserRepository
}
// NewUserUsecase creates a new UserUsecase instance.
func NewUserUsecase(repo UserRepository) *UserUsecase {
return &UserUsecase{repo: repo}
}
// CreateUser handles the business logic of creating a user.
func (uc *UserUsecase) CreateUser(user *entity.User) error {
// Business rule example: email must not be empty
if user.Email == "" {
return ErrInvalidEmail
}
return uc.repo.Create(user)
}
// GetUserByID retrieves a user by ID.
func (uc *UserUsecase) GetUserByID(id int64) (*entity.User, error) {
return uc.repo.GetByID(id)
}
// ListUsers retrieves all users.
func (uc *UserUsecase) ListUsers() ([]entity.User, error) {
return uc.repo.GetAll()
}
2. Define Use Case Errors
In the same usecase
folder, create a errors.go
file for domain-specific errors:
package usecase
import "errors"
var (
ErrInvalidEmail = errors.New("invalid email address")
)
Key Points
-
UserRepository
interface → defines contracts for persistence (but doesn’t care if it’s Postgres, MySQL, or in-memory). -
UserUsecase
struct → implements application business rules using the repository interface. -
Validation/business rules (like checking if an email is empty) live here, not in the controller or repository.
This keeps the business logic framework-agnostic and testable.
✅ Now we’ve defined our use cases.
Define Repository Interfaces (Abstractions)
In Clean Architecture, repositories act as the boundary between the use cases and the infrastructure layer (database, external APIs, etc.).
-
Use Cases should depend only on interfaces, not on concrete implementations.
-
This allows us to switch databases (Postgres, MySQL, MongoDB, or even in-memory) without changing our business logic.
Since we already defined a UserRepository
interface inside the usecase
package, let’s create a dedicated repository
package to keep things clean.
1. Create the repository
package
mkdir -p internal/repository
Inside it, create a new file user_repository.go
:
package repository
import "github.com/yourusername/go-clean-architecture/internal/entity"
// UserRepository defines the contract for persisting users.
// Any implementation (Postgres, MySQL, in-memory) must satisfy this interface.
type UserRepository interface {
Create(user *entity.User) error
GetByID(id int64) (*entity.User, error)
GetAll() ([]entity.User, error)
}
2. Refactor Use Case to Use This Interface
Update internal/usecase/user_usecase.go
so it imports from the repository
package instead of redefining the interface:
package usecase
import (
"github.com/yourusername/go-clean-architecture/internal/entity"
"github.com/yourusername/go-clean-architecture/internal/repository"
)
// UserUsecase contains the business logic for users.
type UserUsecase struct {
repo repository.UserRepository
}
// NewUserUsecase creates a new UserUsecase instance.
func NewUserUsecase(repo repository.UserRepository) *UserUsecase {
return &UserUsecase{repo: repo}
}
// CreateUser handles the business logic of creating a user.
func (uc *UserUsecase) CreateUser(user *entity.User) error {
if user.Email == "" {
return ErrInvalidEmail
}
return uc.repo.Create(user)
}
// GetUserByID retrieves a user by ID.
func (uc *UserUsecase) GetUserByID(id int64) (*entity.User, error) {
return uc.repo.GetByID(id)
}
// ListUsers retrieves all users.
func (uc *UserUsecase) ListUsers() ([]entity.User, error) {
return uc.repo.GetAll()
}
Key Points
-
The
repository
package holds abstract interfaces. -
The
infrastructure
layer (we’ll implement soon) will satisfy these interfaces with actual DB logic. -
The usecase layer now depends only on the repository abstraction, keeping it isolated from frameworks and databases.
✅ At this stage, we’ve clearly separated our contracts (repositories) from their implementations.
Implement Repository (Infrastructure Layer)
The infrastructure layer provides the actual implementation of our UserRepository
interface. This is where we connect to the database (e.g., PostgreSQL) using a library such as database/sql
or gorm
.
For this tutorial, we’ll keep it simple and idiomatic by using database/sql
with pgx
as the PostgreSQL driver.
1. Install Dependencies
Run the following command inside your project:
go get github.com/jackc/pgx/v5/stdlib
This will give us a modern and efficient PostgreSQL driver.
2. Setup Database Connection
Create a new file internal/infrastructure/db.go
:
package infrastructure
import (
"database/sql"
"log"
_ "github.com/jackc/pgx/v5/stdlib"
)
// NewPostgresDB creates a new PostgreSQL connection pool.
func NewPostgresDB(dsn string) *sql.DB {
db, err := sql.Open("pgx", dsn)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
// Verify the connection
if err := db.Ping(); err != nil {
log.Fatalf("failed to ping database: %v", err)
}
return db
}
3. Implement User Repository
Inside the internal/infrastructure
package, create user_repository_postgres.go
:
package infrastructure
import (
"database/sql"
"github.com/yourusername/go-clean-architecture/internal/entity"
"github.com/yourusername/go-clean-architecture/internal/repository"
)
// UserRepositoryPostgres is a Postgres implementation of UserRepository.
type UserRepositoryPostgres struct {
db *sql.DB
}
// NewUserRepositoryPostgres creates a new repository instance.
func NewUserRepositoryPostgres(db *sql.DB) repository.UserRepository {
return &UserRepositoryPostgres{db: db}
}
func (r *UserRepositoryPostgres) Create(user *entity.User) error {
query := "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"
return r.db.QueryRow(query, user.Name, user.Email).Scan(&user.ID)
}
func (r *UserRepositoryPostgres) GetByID(id int64) (*entity.User, error) {
var user entity.User
query := "SELECT id, name, email FROM users WHERE id = $1"
err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepositoryPostgres) GetAll() ([]entity.User, error) {
query := "SELECT id, name, email FROM users"
rows, err := r.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []entity.User
for rows.Next() {
var user entity.User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
4. Database Schema
Before running the app, ensure you have a users table in PostgreSQL:
psql postgres -U djamware
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
\q
Key Points
-
The repository implementation (
UserRepositoryPostgres
) satisfies theUserRepository
interface. -
The infrastructure layer depends on Postgres, but the use case layer remains database-agnostic.
-
We can swap PostgreSQL with MySQL, MongoDB, or even an in-memory repo for testing without changing business logic.
✅ At this point, our repository layer is ready.
Delivery Layer (HTTP Handlers with Gin)
The delivery layer (sometimes called interface adapters) is responsible for:
-
Accepting HTTP requests
-
Mapping requests to use case methods
-
Returning appropriate responses (JSON)
1. Install Gin
Run the following command:
go get github.com/gin-gonic/gin
2. Create HTTP Handlers
Inside internal/handler
, create a file user_handler.go
:
package handler
import (
"net/http"
"strconv"
"github.com/yourusername/go-clean-architecture/internal/entity"
"github.com/yourusername/go-clean-architecture/internal/usecase"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userUsecase *usecase.UserUsecase
}
func NewUserHandler(u *usecase.UserUsecase) *UserHandler {
return &UserHandler{userUsecase: u}
}
// POST /users
func (h *UserHandler) CreateUser(c *gin.Context) {
var user entity.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.userUsecase.CreateUser(&user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// GET /users/:id
func (h *UserHandler) GetUserByID(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseInt(idParam, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
user, err := h.userUsecase.GetUserByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
}
3. Wire Everything Together in main.go
Now we need an entry point to initialize:
-
DB connection
-
Repository
-
Usecase
-
HTTP Handler
Create a cmd/app/main.go
:
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-clean-architecture/internal/handler"
"github.com/yourusername/go-clean-architecture/internal/infrastructure"
"github.com/yourusername/go-clean-architecture/internal/usecase"
)
func main() {
// Setup PostgreSQL connection
dsn := "postgres://username:password@localhost:5432/clean_arch_db?sslmode=disable"
db := infrastructure.NewPostgresDB(dsn)
// Setup repository
userRepo := infrastructure.NewUserRepositoryPostgres(db)
// Setup usecase
userUC := usecase.NewUserUsecase(userRepo)
// Initialize handler
userHandler := handler.NewUserHandler(userUC)
// Setup Gin router
r := gin.Default()
// Routes
r.POST("/users", userHandler.CreateUser)
r.GET("/users/:id", userHandler.GetUserByID)
// Run server
log.Println("Server running at http://localhost:8080")
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
4. Run the API
go run cmd/app/main.go
Your API is now live 🎉
5. Test Endpoints
Using cURL or Postman:
-
Create User
curl -X POST http://localhost:8080/users \ -H "Content-Type: application/json" \ -d '{"name":"Alice","email":"[email protected]"}'
-
Get User by ID
curl http://localhost:8080/users/1
-
Get All Users
curl http://localhost:8080/users
✅ At this stage, we’ve connected all layers:
-
main.go
wires everything together -
Gin delivers the API
-
Use cases contain business logic
-
Repository persists data in Postgres
Adding Persistent Storage (Switching from In-Memory to a Real Database)
Because so far, our repository only stores users in memory, which means data disappears once the app stops. For a scalable API with Clean Architecture, we need a proper persistence layer (e.g., PostgreSQL).
Adding Persistent Storage with PostgreSQL
1. Install PostgreSQL
Make sure you have PostgreSQL installed locally or running in Docker.
For Docker, you can spin it up with:
docker run --name clean-arch-postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=cleanarch -p 5432:5432 -d postgres:16
2. Install Dependencies
We’ll use gorm
(popular ORM in Go):
go get gorm.io/gorm
go get gorm.io/driver/postgres
3. Create a Database Repository Implementation
Inside internal/infrastructure/postgres_user_repository.go
:
package infrastructure
import (
"database/sql"
"github.com/yourusername/go-clean-architecture/internal/entity"
)
type PostgresUserRepository struct {
db *sql.DB
}
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Create(user *entity.User) error {
_, err := r.db.Exec("INSERT INTO users(name, email) VALUES($1, $2)", user.Name, user.Email)
return err
}
func (r *PostgresUserRepository) GetByID(id int64) (*entity.User, error) {
query := `SELECT id, name, email FROM users WHERE id = $1`
row := r.db.QueryRow(query, id)
var user entity.User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *PostgresUserRepository) GetAll() ([]entity.User, error) {
query := "SELECT id, name, email FROM users"
rows, err := r.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []entity.User
for rows.Next() {
var user entity.User
if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
4. Update main.go
to Use PostgreSQL
Modify main.go
:
package main
import (
"log"
"github.com/gin-gonic/gin"
"your_project/internal/handler"
"your_project/internal/infrastructure"
"your_project/internal/usecase"
)
func main() {
// DSN for PostgreSQL
dsn := "host=localhost user=admin password=secret dbname=cleanarch port=5432 sslmode=disable"
// Initialize repository (Postgres)
userRepo, err := infrastructure.NewPostgresUserRepository(dsn)
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
// Initialize usecase
userUC := usecase.NewUserUsecase(userRepo)
// Initialize handler
userHandler := handler.NewUserHandler(userUC)
// Setup Gin router
r := gin.Default()
r.POST("/users", userHandler.CreateUser)
r.GET("/users/:id", userHandler.GetUserByID)
// Run server
log.Println("Server running at http://localhost:8080")
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
✅ With this, your API now uses PostgreSQL instead of memory.
Users will persist in the database across server restarts.
Structuring the Project for Scalability (Clean Architecture)
So far, we’ve kept everything inside internal/
. That works for small apps, but for a real scalable project, we should enforce layer separation and make the app easy to extend (e.g., new features, multiple databases, additional delivery mechanisms like gRPC).
Here’s a recommended folder structure for Clean Architecture in Go:
your_project/
│── cmd/
│ └── api/
│ └── main.go # Entry point
│
│── config/ # Config management (YAML/ENV loader)
│
│── internal/
│ ├── entity/ # Enterprise business rules (core models)
│ │ └── user.go
│ │
│ ├── usecase/ # Application business rules (interactors)
│ │ └── user_usecase.go
│ │
│ ├── repository/ # Interfaces (ports)
│ │ └── user_repository.go
│ │
│ ├── infrastructure/ # External implementations (adapters)
│ │ ├── postgres_user_repository.go
│ │ └── memory_user_repository.go
│ │
│ ├── delivery/ # Delivery mechanisms
│ │ └── http/ # HTTP handler with Gin
│ │ └── user_handler.go
│ │
│ └── bootstrap/ # App wiring (dependency injection)
│ └── app.go
│
│── pkg/ # Reusable packages (logging, utils, middlewares)
│── go.mod
│── go.sum
🔹 1. cmd/api/main.go
(Entry Point)
Keep the main.go
as thin as possible. Only bootstrapping logic:
package main
import (
"log"
"your_project/internal/bootstrap"
)
func main() {
app := bootstrap.InitializeApp()
if err := app.Run(":8080"); err != nil {
log.Fatal(err)
}
}
🔹 2. internal/bootstrap/app.go
(Dependency Injection)
This layer wires everything together:
package bootstrap
import (
"database/sql"
"log"
"github.com/yourusername/go-clean-architecture/internal/handler"
"github.com/yourusername/go-clean-architecture/internal/infrastructure"
"github.com/yourusername/go-clean-architecture/internal/usecase"
"github.com/gin-gonic/gin"
)
type App struct {
engine *gin.Engine
}
func InitializeApp() *App {
dsn := "host=localhost port=5432 user=postgres password=yourpassword dbname=yourdb sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize repository (Postgres)
userRepo := infrastructure.NewPostgresUserRepository(db)
if err != nil {
log.Fatalf("failed to connect database: %v", err)
}
// Initialize usecase
userUC := usecase.NewUserUsecase(userRepo)
// Initialize handler
userHandler := handler.NewUserHandler(userUC)
// Setup Gin router
r := gin.Default()
r.POST("/users", userHandler.CreateUser)
r.GET("/users/:id", userHandler.GetUserByID)
return &App{engine: r}
}
func (a *App) Run(addr string) error {
return a.engine.Run(addr)
}
🔹 3. Config Management (config/
)
Instead of hardcoding DSN in code, load configs from environment variables or YAML.
Example: config/config.yaml
server:
port: 8080
database:
host: localhost
port: 5432
user: admin
password: secret
name: cleanarch
Loader in config/config.go
:
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
Server struct {
Port string
}
Database struct {
Host string
Port int
User string
Password string
Name string
}
}
func LoadConfig() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Error loading config: %v", err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatalf("Error unmarshalling config: %v", err)
}
return &cfg
}
Install. this dependency.
go get github.com/spf13/viper
Then use LoadConfig()
inside bootstrap/app.go
to configure the DB and server dynamically.
🔹 4. Why This Structure Scales
-
Loose coupling: Entities never depend on frameworks (Gin, Gorm).
-
Replaceable layers: Want to switch to MySQL? Just add
mysql_user_repository.go
. -
Multiple delivery mechanisms: Easily add gRPC, CLI, or GraphQL under
delivery/
. -
Clear boundaries: Each layer has a single responsibility.
✅ With this structure, your project can grow into a large-scale system while remaining maintainable and testable.
Unit Testing Strategy for Clean Architecture in Go
One of the biggest benefits of using Clean Architecture is testability. Since the layers are independent and communicate only through abstractions (interfaces), we can easily isolate and test business logic without depending on frameworks, databases, or external services.
🎯 Testing Goals
In this architecture, unit testing should focus on three primary layers:
-
Entity Layer (Core Models)
-
Usually very light, so testing here is minimal.
-
You may only need tests for entity validation methods (if any).
-
-
Use Case Layer (Business Logic)
-
The most important layer to test.
-
Should be tested in isolation by mocking the
Repository
interfaces. -
Verify that business rules (validation, error handling, data flow) work as expected.
-
-
Repository Layer (Infrastructure)
-
Should be tested separately with integration tests (not pure unit tests).
-
Example: using a temporary PostgreSQL or in-memory SQLite to validate SQL queries.
-
-
Delivery Layer (HTTP Handlers)
-
Can be tested with HTTP request simulations (
httptest
in Go). -
These are closer to integration tests, but you can still mock the
Usecase
layer.
-
🏗 Example: Unit Test for Use Case Layer
Let’s test the RegisterUser
use case.
We’ll mock the repository so no database is needed.
usecase/user_usecase_test.go
package usecase_test
import (
"errors"
"testing"
"github.com/didinj/go-clean-architecture/internal/entity"
"github.com/didinj/go-clean-architecture/internal/usecase"
"github.com/didinj/go-clean-architecture/internal/repository"
"github.com/stretchr/testify/assert"
)
// --- Mock Repository ---
type mockUserRepository struct {
users map[int]*entity.User
err error
}
func (m *mockUserRepository) Create(user *entity.User) error {
if m.err != nil {
return m.err
}
user.ID = len(m.users) + 1
m.users[user.ID] = user
return nil
}
func (m *mockUserRepository) GetByID(id int) (*entity.User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("not found")
}
// --- Tests ---
func TestRegisterUser_Success(t *testing.T) {
repo := &mockUserRepository{users: make(map[int]*entity.User)}
uc := usecase.NewUserUsecase(repo)
user := &entity.User{Name: "Alice", Email: "[email protected]"}
err := uc.RegisterUser(user)
assert.NoError(t, err)
assert.Equal(t, 1, user.ID)
}
func TestRegisterUser_Failure(t *testing.T) {
repo := &mockUserRepository{err: errors.New("db error"), users: make(map[int]*entity.User)}
uc := usecase.NewUserUsecase(repo)
user := &entity.User{Name: "Bob", Email: "[email protected]"}
err := uc.RegisterUser(user)
assert.Error(t, err)
assert.Equal(t, "db error", err.Error())
}
✅ This way, we test the use case logic only, without touching the real database.
⚡ Testing HTTP Handlers with httptest
We can also test delivery logic by mocking the usecase layer.
delivery/http/user_handler_test.go
package http_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/didinj/go-clean-architecture/internal/entity"
"github.com/didinj/go-clean-architecture/internal/delivery/http"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
// Mock Usecase
type mockUserUsecase struct{}
func (m *mockUserUsecase) RegisterUser(user *entity.User) error {
user.ID = 99
return nil
}
func (m *mockUserUsecase) GetUser(id int) (*entity.User, error) {
return &entity.User{ID: id, Name: "Test", Email: "[email protected]"}, nil
}
func TestRegisterUserHandler(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.Default()
h := http.NewUserHandler(&mockUserUsecase{})
h.RegisterRoutes(r)
body := `{"name":"Charlie","email":"[email protected]"}`
req, _ := http.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), `"id":99`)
}
📌 Key Takeaways
-
Mock repositories to isolate use cases.
-
Use
httptest
for HTTP handler testing. -
Integration tests should verify repository behavior with a real DB.
-
Entities often need little testing unless they contain logic.
Conclusion
In this guide, you learned how to unit test a Gin handler in a Clean Architecture project by using a mock implementation of the UserUsecase
interface.
The key takeaways are:
-
Always depend on interfaces (like
usecase.UserUsecase
) rather than concrete structs in your handler. -
Create a mock struct that implements the same interface methods to isolate your handler logic from business logic.
-
Use
httptest.NewRecorder
andgin.TestMode
to simulate HTTP requests in your tests. -
Use
testify/assert
to simplify response validation. -
Add a compile-time assertion (
var _ usecase.UserUsecase = (*mockUserUsecase)(nil)
) to guarantee your mock matches the interface contract.
With this approach, you ensure that your handlers are tested independently of your database or real use case logic, leading to faster and more reliable tests.
Next steps:
-
Add more tests for other endpoints (
GetUserHandler
,UpdateUserHandler
, etc.). -
Consider table-driven tests for covering multiple scenarios.
-
Integrate with CI/CD to automatically run your tests on every commit.
You can get the full source code on our GitHub.
That is just the basics. If you need more deep learning about the Golang (Go) language and frameworks, you can take the following cheap course:
- Real-World GoLang Project: Car Management System
- Building GUI Applications with Fyne and Go (Golang)
- AWS Cognito Using Golang
- Google Go Programming for Beginners (golang)
- Building a module in Go (Golang)
- Go/Golang Professional Interview Questions
Thanks!