Building a CRUD App with Clean Architecture in Go

michinoins

Mikiya Ichino

Posted on May 6, 2023

Building a CRUD App with Clean Architecture in Go

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

Table of tech stack and type

What is Clean Architecture?

Clean Architecture is well known for the following diagram:

Diagram of Clean Architecture

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

Directory Structure

The layers of Clean Architecture can be aligned with the directory structure as shown below:

Table of Directory name and layer

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"`
} 
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"))
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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{})
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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{})
}
Enter fullscreen mode Exit fullscreen mode

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"
}'
Enter fullscreen mode Exit fullscreen mode

GET: /users

curl --location --request GET localhost:1323/users'

Enter fullscreen mode Exit fullscreen mode

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…!

💖 💪 🙅 🚩
michinoins
Mikiya Ichino

Posted on May 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related