Building a CRUD App with Clean Architecture in Go
Mikiya Ichino
Posted on May 6, 2023
Introduction
In this article, I will create an API with CRUD (Create, Read, Update, Delete) functionality using Clean Architecture. I will use MySQL as our database, Echo as our framework, and GORM as our ORM.
What I Will Build
I will create an API with Create, Read, (Update), and Delete functionality. The Update function is not implemented yet, so feel free to add it yourself!
Target Audience
People who have set up a Go environment and want to create a simple API.
Technologies Used
What is Clean Architecture?
Clean Architecture is well known for the following diagram:
The purpose of Clean Architecture is the separation of concerns, which is achieved by paying attention to the dependencies between layers. This separation leads to improved code readability and a more robust design. For more information on the benefits and details of Clean Architecture, please refer to the reference articles.
In the above diagram, arrows point from the outer layers to the inner layers, indicating the direction of dependencies. Dependencies are allowed from the outer layers to the inner layers, but not vice versa.
In other words, you can call items declared in the inner layers from the outer layers, but you cannot call items declared in the outer layers from the inner layers.
In this article, I will introduce the code while paying attention to the direction of dependencies.
Endpoints
The endpoints for each function are as follows:
GET: /users
POST: /users
DELETE: /users/:id
Directory Structure
The layers of Clean Architecture can be aligned with the directory structure as shown below:
domain
In the domain layer, the Entity is defined. As it is at the center, it can be called from any layer.
/src/domain/user.go
package domain
type User struct {
ID int `json:"id" gorm:"primary_key"`
Name string `json:"name"`
}
In this example, I will create a User with an ID and Name column, with the ID set as the primary key.
Regarding json:"id" gorm:"primary_key", json:"id" is for JSON mapping, and gorm:"primary_key" is for tagging models in GORM. You can also define other SQL table properties such as not null, unique, and default.
Reference: GORM Declaring Model
Infrastructure
The outermost layer, Infrastructure, deals with the parts of the application that interact with external components. In this case, I define the database connection and router here.
Since it is the outermost layer, it can be called without being aware of any other layers.
/src/infrastructure/sqlhandler.go
package infrastructure
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"echoSample/src/interfaces/database"
)
type SqlHandler struct {
db *gorm.DB
}
func NewSqlHandler() database.SqlHandler {
dsn := "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err.Error)
}
sqlHandler := new(SqlHandler)
sqlHandler.db = db
return sqlHandler
}
func (handler *SqlHandler) Create(obj interface{}) {
handler.db.Create(obj)
}
func (handler *SqlHandler) FindAll(obj interface{}) {
handler.db.Find(obj)
}
func (handler *SqlHandler) DeleteById(obj interface{}, id string) {
handler.db.Delete(obj, id)
}
For the database connection, the official documentation was helpful:
gorm.io
Next is routing. In this example, I use the Echo web framework. The official documentation was also helpful for this, and it is where I define our API methods and paths:
echo.labstack
/src/infrastructure/router.go
package infrastructure
import (
controllers "echoSample/src/interfaces/api"
"net/http"
"github.com/labstack/echo"
)
func Init() {
// Echo instance
e := echo.New()
userController := controllers.NewUserController(NewSqlHandler())
e.GET("/users", func(c echo.Context) error {
users := userController.GetUser()
c.Bind(&users)
return c.JSON(http.StatusOK, users)
})
e.POST("/users", func(c echo.Context) error {
userController.Create(c)
return c.String(http.StatusOK, "created")
})
e.DELETE("/users/:id", func(c echo.Context) error {
id := c.Param("id")
userController.Delete(id)
return c.String(http.StatusOK, "deleted")
})
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
Interfaces
In the Controllers and Presenters layers, We need to be aware of dependencies.
Calling the domain and usecase layers from the interface layer is not a problem, but we cannot call the infrastructure layer directly. Instead, we define an interface (the sqlHandler interface defined in the infrastructure layer).
/src/interfaces/api/user_controller.go
package controllers
import (
"echoSample/src/domain"
"echoSample/src/interfaces/database"
"echoSample/src/usecase"
"github.com/labstack/echo"
)
type UserController struct {
Interactor usecase.UserInteractor
}
func NewUserController(sqlHandler database.SqlHandler) *UserController {
return &UserController{
Interactor: usecase.UserInteractor{
UserRepository: &database.UserRepository{
SqlHandler: sqlHandler,
},
},
}
}
func (controller *UserController) Create(c echo.Context) {
u := domain.User{}
c.Bind(&u)
controller.Interactor.Add(u)
createdUsers := controller.Interactor.GetInfo()
c.JSON(201, createdUsers)
return
}
func (controller *UserController) GetUser() []domain.User {
res := controller.Interactor.GetInfo()
return res
}
func (controller *UserController) Delete(id string) {
controller.Interactor.Delete(id)
}
The controller calls the usecase and domain layers, which is not a problem.
src/interfaces/api/context.go
package controllers
type Context interface {
Param(string) string
Bind(interface{}) error
Status(int)
JSON(int, interface{})
}
Regarding the database:
src/interfaces/database/user_repository.go
package database
import (
"echoSample/src/domain"
)
type UserRepository struct {
SqlHandler
}
func (db *UserRepository) Store(u domain.User) {
db.Create(&u)
}
func (db *UserRepository) Select() []domain.User {
user := []domain.User{}
db.FindAll(&user)
return user
}
func (db *UserRepository) Delete(id string) {
user := []domain.User{}
db.DeleteById(&user, id)
}
In the repository, the sqlHandler is called, but instead of directly calling the infrastructure layer's sqlHandler, we call it through the sqlHandler interface defined in the same layer.
This is known as the Dependency Inversion Principle.
src/interfaces/db/sql_handler.go
package database
type SqlHandler interface {
Create(object interface{})
FindAll(object interface{})
DeleteById(object interface{}, id string)
}
With this, we can call the sql_handler's functions.
usecase
Lastly, we have the usecase layer.
src/usecase/user_interactor.go
package usecase
import "echoSample/src/domain"
type UserInteractor struct {
UserRepository UserRepository
}
func (interactor *UserInteractor) Add(u domain.User) {
interactor.UserRepository.Store(u)
}
func (interactor *UserInteractor) GetInfo() []domain.User {
return interactor.UserRepository.Select()
}
func (interactor *UserInteractor) Delete(id string) {
interactor.UserRepository.Delete(id)
}
Here, as before, we need to apply the Dependency Inversion Principle, so we define user_repository.go.
src/usecase/user_repository.go
package usecase
import (
"echoSample/src/domain"
)
type UserRepository interface {
Store(domain.User)
Select() []domain.User
Delete(id string)
}
With this, our implementation is complete.
Now, by running MySQL using docker-compose.yml and starting the server, the API should work.
version: "3.6"
services:
db:
image: mysql:5.7
container_name: go_sample
volumes:
# mysql configuration
- ./mysql/conf:/etc/mysql/conf.d
- ./mysql/data:/var/lib/mysql
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- 3306:3306
environment:
MYSQL_DATABASE: go_sample
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: root
TZ: "Asia/Tokyo"
src/server.go
package main
import (
"echoSample/src/domain"
"echoSample/src/infrastructure"
"github.com/labstack/echo/v4"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var (
db *gorm.DB
err error
dsn = "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"
)
func main() {
dbinit()
infrastructure.Init()
e := echo.New()
e.Logger.Fatal(e.Start(":1323"))
}
func dbinit() {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
}
db.Migrator().CreateTable(domain.User{})
}
Run Mysql
docker-compose up -d
Run Server
go run serve.go
Call API
POST: /users
curl --location --request POST 'localhost:1323/users' \
--header 'Content-Type: application/json' \
--data-raw '{
"name":"J.Y Park"
}'
curl --location --request POST 'localhost:1323/users' \
--header 'Content-Type: application/json' \
--data-raw '{
"name":"Eron Mask"
}'
GET: /users
curl --location --request GET localhost:1323/users'
It works!
Conclusion
By not only reading articles about Clean Architecture but also actually creating a simple API and testing its functionality, your understanding will deepen.
However, to be honest, the advantage of Clean Architecture might not be fully appreciated when working on a project of the CRUD app scale.
Clean Architecture not only improves code readability and productivity but also has the characteristic of being resistant to change. So, it would be nice to experience its benefits by adding various features to the app I created this time…!
Posted on May 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.