Go-Svelte Journey : Golang Backend with Hexagonal Architecture (Part-2)
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
- 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.