Go-Svelte Journey : Golang Backend with Hexagonal Architecture (Part-2)

Angga Kesuma
9 min readAug 24, 2020

from previous article we have discuss about project structure. In this article I want share how I build API with golang and hexagonal architecture.

Previous Article:

Hexagonal Architecture :

From my perspective, hexagonal architecture when your business logic (domain) can be implemented to many presentation layer (api, pubsub, cli, etc). This architecture is different from MVC architecture that the business logic are at controller, where the controller directly implemented to http API.

If you interested more about this architecture, I have some nice article for you :

Prepare Our Backend :

first, init go mod. to learn more about go mod here. from root folder run :

go mod init github.com/anggakes/gosvelte-todoapp

you can change “github.com/anggakes/gosvelte-todoapps” with your project name.

create 3 folder cmd, dbmodels, pkg

  1. golang as cli application
go get github.com/spf13/cobra v1.0.0
go get github.com/spf13/viper v1.7.1

with 2 package above, we can easily manage our apps as cli and manage our configuration.

create src/backend/main.go

package main

import "github.com/anggakes/gosvelte-todoapps/src/backend/cmd"

func main() {
cmd.Execute()
}

create src/backend/cmd/root.go

package cmd

import (
"fmt"
"github.com/spf13/cobra"
"os"
)

var rootCmd = &cobra.Command{
Use: "app",
Short: "This is app to run",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

create src/backend/cmd/cli.go

package cmd

import (
"context"
"fmt"
)

func init() {
rootCmd.AddCommand(versionCmd)
}

var versionCmd = &cobra.Command{
Use: "cli",
Short: "Run backend service",
Long: `run backend service`,
Run: func(cmd *cobra.Command, args []string) {

fmt.Println("backend service is running")

},
}

try run our application :

go run src/backend/main.go cli

you can see the result below :

2. Build our domain model

create folder src/backend/pkg/models

src/backend/models/todo.go

package models

type Todo struct {
ID int `json:"id" `
Title string `json:"title"`
Description string `json:"description"`
}

type TodoCreateCommand struct {
Title string `json:"title"`
Description string `json:"description"`
}

type TodoUpdateCommand struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
}

type TodoGetQuery struct {
ID string `json:"id"`
}

type TodoListQuery struct {
Size int `json:"size"`
Page int `json:"page"`
}

type TodoListResults struct {
CurrentPage int `json:"current_page"`
Results []Todo `json:"results"`
}

this is our struct for todo with command and query struct.

our application just simple CRUD for todo model. so create folder create folder src/backend/pkg/domains/todo

src/backend/pkg/domains/todo/todo.go

package todo

import (
"context"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/models"
"github.com/pkg/errors"
)

var (
ErrExist = errors.New("Todo already exist")
)

type ITodo interface {
Get(ctx context.Context, query *models.TodoGetQuery) (*models.Todo, error)
Create(ctx context.Context, req *models.TodoCreateCommand) (*models.Todo, error)
Update(ctx context.Context, req *models.TodoUpdateCommand) (*models.Todo, error)
Delete(ctx context.Context, req *models.TodoGetQuery) (*models.Todo, error)
List(ctx context.Context, req *models.TodoListQuery) (*models.TodoListResults, error)
}

func New() ITodo {

// register event

return &service{

}
}

var (
id = 0
td = map[int]models.Todo{}
)

type service struct {
}

func (c *service) Update(ctx context.Context, query *models.TodoUpdateCommand) (*models.Todo, error) {

t := td[query.ID]

t.Description = query.Description
t.Title = query.Title

td[query.ID] = t

return &t, nil

}

func (c *service) Delete(ctx context.Context, query *models.TodoGetQuery) (*models.Todo, error) {
t := td[query.ID]

delete(td, t.ID)

return &t, nil
}

func (c *service) List(ctx context.Context, req *models.TodoListQuery) (*models.TodoListResults, error) {

//extract result
res := []models.Todo{}
for _, v := range td {
res = append(res, v)
}

return &models.TodoListResults{Results: res}, nil
}

func (c *service) Create(ctx context.Context, req *models.TodoCreateCommand) (*models.Todo, error) {

id += 1
t := models.Todo{
ID: id,
Title: req.Title,
Description: req.Description,
}

td[id] = t

return &t, nil
}

func (c *service) Get(ctx context.Context, query *models.TodoGetQuery) (*models.Todo, error) {
t := td[query.ID]
return &t, nil
}

we create todo interface, so other domains will communication only via it.

let’s update our cli command

src/backend/cmd/cli.go

package cmd

import (
"context"
"fmt"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/domains/todo"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/models"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(versionCmd)
}

var versionCmd = &cobra.Command{
Use: "cli",
Short: "Run backend service",
Long: `run backend service`,
Run: func(cmd *cobra.Command, args []string) {

t := todo.New()
ctx := context.Background()

fmt.Println(t.Create(ctx, &models.TodoCreateCommand{
Title: "Title 1",
Description: "description 1",
}))

fmt.Println(t.Create(ctx, &models.TodoCreateCommand{
Title: "Title 2",
Description: "description 2",
}))


fmt.Println("----")

fmt.Println(t.List(ctx, &models.TodoListQuery{}))

fmt.Println(t.Update(ctx, &models.TodoUpdateCommand{
ID:2,
Title: "Title 3",
Description: "description 3",
}))

fmt.Println(t.Get(ctx, &models.TodoGetQuery{
ID: 2,
}))

},
}

try run our application :

go run src/backend/main.go cli

you can see the result below :

Run migration and generate dbmodels

we already success isolate our business login within domain and models.

easy right next lets implement our webserver and database.

let’s run our migration, migration have been discuss here

just run our docker compose to run our postgresql:

docker-compose up -d

lets install our migration :

cd migrations
docker-compose up

For ORM I recommended this one, https://github.com/volatiletech/sqlboiler. I love it because this package is generated. I believe this one will be faster than other.

# Install sqlboiler v4
GO111MODULE=off go get -u -t github.com/volatiletech/sqlboiler
# Install an sqlboiler driver - these are seperate binaries, here we are
# choosing postgresql
GO111MODULE=off go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql
# Do not forget the trailing /v4
go get github.com/volatiletech/sqlboiler/v4
# Assuming you're going to use the null package for its additional null types
# Do not forget the trailing /v8
go get github.com/volatiletech/null/v8

create our sqlboiler configuration:

sqlboiler.toml

output   = "src/backend/dbmodels"
pkgname = "dbmodels"

[psql]
dbname = "gosvelte"
host = "localhost"
port = 5432
user = "user"
pass = "password"
sslmode = "disable"
schema = "public"
blacklist = [
"django_migrations",
]

generate our generated dbmodel :

rm -rf src/backend/dbmodels/*
sqlboiler psql

this command will create file in folder src/backend/dbmodels/. nice and easy right !

update our todo domain to implement dbmodels

src/backend/pkg/domains/todo/todo.go

package todo

import (
"context"
"database/sql"
"github.com/anggakes/gosvelte-todoapps/src/backend/dbmodels"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/models"
"github.com/pkg/errors"
"github.com/ulule/deepcopier"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
)

var (
ErrExist = errors.New("Todo already exist")
)

type ITodo interface {
Get(ctx context.Context, query *models.TodoGetQuery) (*models.Todo, error)
Create(ctx context.Context, req *models.TodoCreateCommand) (*models.Todo, error)
Update(ctx context.Context, req *models.TodoUpdateCommand) (*models.Todo, error)
Delete(ctx context.Context, req *models.TodoGetQuery) (*models.Todo, error)
List(ctx context.Context, req *models.TodoListQuery) (*models.TodoListResults, error)
}

func New(db *sql.DB) ITodo {

// register event

return &service{
DB: db,
}
}

type service struct {
DB *sql.DB
}

func (c *service) Update(ctx context.Context, req *models.TodoUpdateCommand) (*models.Todo, error) {

tMod, err := dbmodels.Todos(qm.Where("id=?", req.ID)).One(ctx, c.DB)
if err != nil {
return nil, err
}

tMod.Description = req.Description
tMod.Title = req.Title

_, err = tMod.Update(ctx, c.DB, boil.Infer())
if err != nil {
return nil, err
}

md := &models.Todo{}

if err := deepcopier.Copy(tMod).To(md); err != nil {
return nil, err
}

return md, nil

}

func (c *service) Delete(ctx context.Context, req *models.TodoGetQuery) (*models.Todo, error) {

tMod, err := dbmodels.Todos(qm.Where("id=?", req.ID)).One(ctx, c.DB)
if err != nil {
return nil, err
}

md := &models.Todo{}
if err := deepcopier.Copy(tMod).To(md); err != nil {
return nil, err
}

_, err = tMod.Delete(ctx, c.DB)
if err != nil {
return nil, err
}

return md, nil

}

func (c *service) List(ctx context.Context, req *models.TodoListQuery) (*models.TodoListResults, error) {

skip := req.Size * (req.Page - 1)

tdr := &models.TodoListResults{
CurrentPage: req.Page,
Results: []models.Todo{},
}

tdRes, err := dbmodels.Todos(qm.Limit(req.Size), qm.Offset(skip), qm.OrderBy("id DESC")).All(ctx, c.DB)
if err != nil {
return nil, err
}

for _, v := range tdRes {
md := models.Todo{}

if err := deepcopier.Copy(v).To(&md); err != nil {
return nil, err
}

tdr.Results = append(tdr.Results, md)
}

return tdr, nil

}

func (c *service) Create(ctx context.Context, req *models.TodoCreateCommand) (*models.Todo, error) {

tMod := dbmodels.Todo{
Title: req.Title,
Description: req.Description,
}

if err := tMod.Insert(ctx, c.DB, boil.Infer()); err != nil {
return nil, err
}

md := &models.Todo{}

if err := deepcopier.Copy(tMod).To(md); err != nil {
return nil, err
}

return md, nil
}

func (c *service) Get(ctx context.Context, query *models.TodoGetQuery) (*models.Todo, error) {

tMod, err := dbmodels.Todos(qm.Where("id=?", query.ID)).One(ctx, c.DB)
if err != nil {
return nil, err
}

md := &models.Todo{}

if err := deepcopier.Copy(tMod).To(md); err != nil {
return nil, err
}

return md, nil
}

I use deepcopier for copy from sqlboiler model to our business model. https://github.com/ulule/deepcopier

let’s create dependencies provider

create folder src/backend/pkg/providers

src/backend/pkg/providers/config.go

package providers

import "github.com/spf13/viper"

type Config struct {
DBPort string
DBHost string
DBUser string
DBName string
DBPassword string
}

func LoadConfig() *Config {
// set default
config := &Config{
DBPort: "5432",
DBName: "gosvelte",
DBUser: "user",
DBPassword: "password",
DBHost: "localhost",
}
if err := viper.Unmarshal(config); err != nil {
return nil
}
return config
}

src/backend/pkg/providers/provider.go

package providers

import (
"database/sql"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/domains/todo"
_ "github.com/lib/pq"
)

type Provider struct {
Todo todo.ITodo
}

func InitProvider() *Provider {

cfg := LoadConfig()

// init DB
DBServer := "host=" + cfg.DBHost + " port=" + cfg.DBPort + " user=" + cfg.DBUser + " dbname=" + cfg.DBName + " password=" + cfg.DBPassword + " sslmode=disable"
db, err := sql.Open("postgres", DBServer)
if err != nil {
panic("failed connect DB : " + err.Error())
}

if err := db.Ping(); err != nil {
panic("failed connect DB : " + err.Error())
}

td := todo.New(db)

return &Provider{
Todo: td,
}
}

this is where we manage dependency injection. we will inject provider struct to handlers

Cook Our WebServer

For web server I’ll use go echo https://echo.labstack.com/

go get -u github.com/labstack/echo/...

create handler and routes:

src/backend/pkg/handlers/http/todo.go

package httpimport (
"fmt"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/models"
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/domains/todo"
"github.com/labstack/echo/v4"
"net/http"
"strconv"
)
type TodoHandler struct {
TodoUseCase todo.ITodo
}
func (c *TodoHandler) Get(e echo.Context) error { ctx := e.Request().Context()
idStr := e.Param("id")
id, _:= strconv.Atoi(idStr) td, err := c.TodoUseCase.Get(ctx, &models.TodoGetQuery{
ID: id,
})
if err != nil {
fmt.Println(err)
return err
}
return e.JSON(http.StatusOK, td)
}
func (c *TodoHandler) Create(e echo.Context) error { ctx := e.Request().Context() req := &models.TodoCreateCommand{} if err := e.Bind(req); err != nil {
return echo.ErrBadRequest
}
td, err := c.TodoUseCase.Create(ctx, req)
if err != nil {
return err
}
return e.JSON(http.StatusOK, td)
}
func (c *TodoHandler) List(e echo.Context) error { ctx := e.Request().Context() page, _ := strconv.Atoi(e.QueryParam("page"))
size, _ := strconv.Atoi(e.QueryParam("size"))
if page == 0 {
page = 1
}
if size == 0 {
size = 5
}
td, err := c.TodoUseCase.List(ctx, &models.TodoListQuery{
Page: page,
Size: size,
})
if err != nil {
return err
}
return e.JSON(http.StatusOK, td)}func (c *TodoHandler) Update(e echo.Context) error { ctx := e.Request().Context() req := &models.TodoUpdateCommand{}
if err := e.Bind(req); err != nil {
return echo.ErrBadRequest
}
req.ID, _ = strconv.Atoi(e.Param("id")) td, err := c.TodoUseCase.Update(ctx, req)
if err != nil {
return err
}
return e.JSON(http.StatusOK, td)
}
func (c *TodoHandler) Delete(e echo.Context) error { ctx := e.Request().Context() idStr := e.Param("id") id, _:= strconv.Atoi(idStr) td, err := c.TodoUseCase.Delete(ctx, &models.TodoGetQuery{
ID: id,
})
if err != nil {
return err
}
return e.JSON(http.StatusOK, td)
}

src/backend/pkg/handlers/http/route.go

package http

import (
"github.com/anggakes/gosvelte-todoapps/src/backend/pkg/providers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

func RegisterRoute(e *echo.Echo, prv *providers.Provider) {

e.Use(middleware.CORS())

todoHndlr := &TodoHandler{prv.Todo}
todo := e.Group("/todos")
todo.Add("GET", "/:id/", todoHndlr.Get)
todo.Add("POST", "/", todoHndlr.Create)
todo.Add("DELETE", "/:id/", todoHndlr.Delete)
todo.Add("PUT", "/:id/", todoHndlr.Update)
todo.Add("GET", "/", todoHndlr.List)

}

we inject our provider to route and inject dependencies to our domain here.

run our cmd:

go run src/backend/main.go webserver# if you found error from cli.go, just comment it now

Test it with postman :

Conclusion :

that’s it we have finish build our API. what we’ve learned are how to manage our go projects, using cli, orm with sqlboiler, and webserver with go echo with this architecture we have isolate our business logic, so our business logic not only can be consume by our API but from CLI too, and of course other handler later.

New article I will share how we consume our API with svelte.

--

--

Angga Kesuma

learn, do, and share ~ Senior Software Engineer @KompasGramedia