[Golang] How to Build Rest API Using Golang and Mongo DB With the Concept of Clean Architecture

Amiruddin Saddam
Learning About Golang
4 min readMay 10, 2021

--

As we all know, neat and good architecture is important for a system. because if the system architecture is not good, it will have a bad impact in the future.

Likewise with the Rest API, the better the architecture that is built, it will make it easier for developers to do maintenance and can also improve performance.

One of the well-known clean architectural concepts used by Robert C. Martin or commonly known as Uncle Bob.

Based on this concept, we will apply the 4 main uncle bob circles as follows:

1.Model as an Entity

In this section, we can write a struct which is a collection of definitions of variables, properties or methods in golang.

package model

type (
GetDataResponse struct {
Data []DataProduct `json:"product"`
}
DataProduct struct {
Name string `json:"name" bson:"name"`
}
)

On this page, the related entities will be defined as well as the attributes / properties they have

2.UseCase

UseCase functions to connect the business process flow with the data flow that is processed in the repository section which is also connected to the related entity.

package crud

import (
"context"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
)

type CrudUseCaseI interface {
GetDataUC(ctx context.Context) (resp model.GetDataResponse, err error)
}

In this section, we can create an interface type to define the methods contained in it.

package usecase

import (
"context"
"github.com/asdamwongmantap/api-echo-mongo/crud"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
)

type CrudUseCase struct {
config *model.EnvConfig
crudRepo crud.CrudRepositoryI
}

func NewCrudUseCase(config *model.EnvConfig, crudRepo crud.CrudRepositoryI) crud.CrudUseCaseI {
return &CrudUseCase{
config: config,
crudRepo: crudRepo,
}
}

func (cuc *CrudUseCase) GetDataUC(ctx context.Context) (resp model.GetDataResponse, err error) {
if ctx == nil {
ctx = context.Background()
}
list, err := cuc.crudRepo.GetAllData(ctx)
if err != nil {
return resp, err
}

return list, err
}

3.Controller and Delivery as Interface Adapters

  • Controller

The controller is a link between delivery as a presenter and usecase which functions to regulate business process flows and data flows.

package controller

import (
"context"
"encoding/json"
"github.com/asdamwongmantap/api-echo-mongo/crud"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
"github.com/labstack/echo/v4"
)

type CrudController struct {
e *echo.Echo
usecase crud.CrudUseCaseI
}

func NewCrudController(e *echo.Echo, usecase crud.CrudUseCaseI) *CrudController {
return &CrudController{
e: e,
usecase: usecase,
}
}

func (cc *CrudController) GetData(ec echo.Context) error {

data, err := cc.usecase.GetDataUC(context.Background())
if err != nil {
return err
}

return ec.JSON(200, data)
}
  • Delivery

As previously mentioned, delivery acts as a presenter whose function is to define the endpoints that can be used for the front page.

package http

import (
"github.com/asdamwongmantap/api-echo-mongo/crud"
"github.com/asdamwongmantap/api-echo-mongo/crud/delivery/controller"
"github.com/labstack/echo/v4"
)

func NewRouter(e *echo.Echo, crudUseCase crud.CrudUseCaseI) {

crudCtrl := controller.NewCrudController(e, crudUseCase)

r := e.Group("/api/v1/go-mongo")
r.GET("/list", crudCtrl.GetData)
}

4.Repository as a Framework and Drivers

Just like in the usecase section, we can also create an interface type to define the methods contained in it.

package crud

import (
"context"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
)

type CrudRepositoryI interface {
GetAllData(ctx context.Context) (crudResp model.GetDataResponse, err error)
}

Repository functions to process data obtained from the database. on this occasion we are using mongoDB as the database.

How to connect to the mongoDB database ?

Just like the way we connect to other databases, in mongoDB we also have to define host, name, password, port as below:

func Connect(c MongoConfig) (*mongo.Database, error) {
connPattern := "mongodb://%v:%v@%v:%v"
if c.Username == "" {
connPattern = "mongodb://%s%s%v:%v"
}

clientUrl := fmt.Sprintf(connPattern,
c.Username,
c.Password,
c.Host,
c.Port,
)
clientOptions := options.Client().ApplyURI(clientUrl)
client, err := mongo.NewClient(clientOptions)
if err != nil {
return nil, err
}

ctx, _ := context.WithTimeout(context.Background(), time.Duration(c.Timeout)*time.Second)
err = client.Connect(ctx)
if err != nil {
return nil, err
}

return client.Database(c.DBname), err
}

After we create a link function, we can apply it to the main.go page

package main

import (
"fmt"
httpDelivery "github.com/asdamwongmantap/api-echo-mongo/crud/delivery/http"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
"github.com/asdamwongmantap/api-echo-mongo/crud/repository"
"github.com/asdamwongmantap/api-echo-mongo/crud/usecase"
"github.com/asdamwongmantap/api-echo-mongo/lib/db"
"github.com/labstack/echo/v4/middleware"
"log"
"net/http"

"github.com/labstack/echo/v4"
)

func main() {
envConfig := getConfig()

e := echo.New()

// Mongo
mongo, err := db.Connect(envConfig.Mongo)
if err != nil {
log.Println(err)
return
}

crudRepo := repository.NewCrudRepository(mongo)
crudUseCase := usecase.NewCrudUseCase(&envConfig, crudRepo)
// Router
httpDelivery.NewRouter(e, crudUseCase)

e.Logger.Fatal(e.Start(fmt.Sprintf("%s%s%v",envConfig.Host,":",envConfig.Port)))
}

func getConfig() model.EnvConfig {

return model.EnvConfig{
Host: "0.0.0.0",
Port: 9595,
Mongo: db.MongoConfig{
Timeout: 5000,
DBname: "crud_learn",
Username: "xxx",
Password: "xxx",
Host: "0.0.0.0",
Port: "27017",
},
}
}

Then how to apply the query process to golang ?

package repository

import (
"context"
"github.com/asdamwongmantap/api-echo-mongo/crud"
"github.com/asdamwongmantap/api-echo-mongo/crud/model"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"log"
)

type CrudRepository struct {
mongoDB *mongo.Database
}

func NewCrudRepository(mongo *mongo.Database) crud.CrudRepositoryI {
return &CrudRepository{
mongoDB: mongo,
}
}

func (cr CrudRepository) GetAllData(ctx context.Context) (crudResp model.GetDataResponse, err error) {

query, err := cr.mongoDB.Collection("product").Find(ctx, bson.D{})
if err != nil {
log.Println("error", err)
return model.GetDataResponse{}, err
}
defer query.Close(ctx)

listDataProduct := make([]model.DataProduct, 0)
for query.Next(ctx) {
var row model.DataProduct
err := query.Decode(&row)
if err != nil {
log.Println("error")
}
listDataProduct = append(listDataProduct, row)
}

crudResp = model.GetDataResponse{Data: listDataProduct}

return crudResp, err
}

Conclusion:

By grouping them based on their functions and roles, the developer will not find it difficult to carry out maintenance. and also expected to be able to improve performance on the Rest API that was made.

Happy Coding :)

Reference:

  1. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
  2. https://hackernoon.com/golang-clean-archithecture-efd6d7c43047
  3. https://dasarpemrogramangolang.novalagung.com/A-struct.html

--

--