[Golang] How to Build Rest API Using Golang and Mongo DB With the Concept of Clean Architecture
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: