Building a CRUD App with MySQL, GORM, Echo, and 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…


This content originally appeared on DEV Community and was authored by Mikiya Ichino

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"`
} 

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


This content originally appeared on DEV Community and was authored by Mikiya Ichino


Print Share Comment Cite Upload Translate Updates
APA

Mikiya Ichino | Sciencx (2023-05-06T04:47:11+00:00) Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go. Retrieved from https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/

MLA
" » Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go." Mikiya Ichino | Sciencx - Saturday May 6, 2023, https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/
HARVARD
Mikiya Ichino | Sciencx Saturday May 6, 2023 » Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go., viewed ,<https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/>
VANCOUVER
Mikiya Ichino | Sciencx - » Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/
CHICAGO
" » Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go." Mikiya Ichino | Sciencx - Accessed . https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/
IEEE
" » Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go." Mikiya Ichino | Sciencx [Online]. Available: https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/. [Accessed: ]
rf:citation
» Building a CRUD App with MySQL, GORM, Echo, and Clean Architecture in Go | Mikiya Ichino | Sciencx | https://www.scien.cx/2023/05/06/building-a-crud-app-with-mysql-gorm-echo-and-clean-architecture-in-go/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.