Go Microservice Template for Beginners by .NET Developer. Part 2
Now we move from describing what we want to implement to what our microservice will look like.
Let's start with how the client will interact with our bookstore. For interaction, we choose the REST API, which will include the following requests:
GET /api/v1/books – Receiving a list of books available for sale by the client.
POST /api/v1/books – Adding a new book to our store.
POST /api/v1/books/buy – Purchase of a book contained in our store
In my microservice architecture, I decided to abandon the classic service approach in favor of CQRS using the MediatR library, where the API contracts will be commands (Command) and queries (Query). This solution fits very well with microservices in .NET, and I think that in Go it will not cause any particular difficulties either, since I have seen many projects on GitHub that work on this basis.
We will simulate the delivery of books using a background task running alongside the HTTP server. This task will send new books to Kafka, and our consumer will store them in our store. I added this element to show an example of interaction with Kafka, so no claims about the appropriateness of using Kafka here.
The tests I prepared for this project are component tests. This is a kind of analogue of end-to-end tests, but all the necessary environment for the application is raised in containers, simulating the work of a real API. You can learn about what component tests are in the next article.
However, they will be in continuation of this template and in the next separate article, as well as the part with kafka.
Begin
After looking at various real projects on GitHub, I decided to deviate a bit from the structure I described in the previous article. I thought about it for a long time and came to the following decision.
First, let's create a project and add the first directory to it. cmd
in which we will place our main file main.go
. The equivalent in the latest versions of .NET is a file Program.cs
in which our server configuration takes place.
Our service will be configured using the “go.uber.org/fx” library, so let's open the console and run the following command:
go get "go.uber.org/fx"
We fill main.go as follows.
package main
import "go.uber.org/fx"
func main() {
fx.New(
fx.Options(
fx.Provide(),
),
).Run()
}
Uber FX is a wrapper in which we register all the necessary dependencies and also launch tasks for processing.
To make it clearer, I'll show you how it would look in .NET:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run();
We need to configure our HTTP server that will handle incoming requests. For this purpose, I decided to use the Echo library.
go get "github.com/labstack/echo/v4"
go get "github.com/labstack/gommon/log"
We go to the directory where our common service packages will be located pkg
and create a catalog http
in which we create a directory server
with file echo_server.go
.
package echoserver
import (
"context"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"time"
)
const (
MaxHeaderBytes = 1 << 20
ReadTimeout = 15 * time.Second
WriteTimeout = 15 * time.Second
)
type EchoConfig struct {
Port string `mapstructure:"port" validate:"required"`
Development bool `mapstructure:"development"`
BasePath string `mapstructure:"basePath" validate:"required"`
DebugErrorsResponse bool `mapstructure:"debugErrorsResponse"`
IgnoreLogUrls []string `mapstructure:"ignoreLogUrls"`
Timeout int `mapstructure:"timeout"`
Host string `mapstructure:"host"`
}
func NewEchoServer() *echo.Echo {
e := echo.New()
return e
}
// RunHttpServer - запустить наш HTTP-сервер
func RunHttpServer(ctx context.Context, echo *echo.Echo, cfg *EchoConfig) error {
echo.Server.ReadTimeout = ReadTimeout
echo.Server.WriteTimeout = WriteTimeout
echo.Server.MaxHeaderBytes = MaxHeaderBytes
go func() {
for {
select {
case <-ctx.Done():
log.Infof("Сервер завершает свою работу. HTTP POST: {%s}", cfg.Port)
err := echo.Shutdown(ctx)
if err != nil {
log.Errorf("(ОТКЛЮЧЕНИЕ СЕРВЕРА) ошибка: {%v}", err)
return
}
return
}
}
}()
err := echo.Start(cfg.Port)
return err
}
This code contains the configuration of our server and the function for direct launch.
We also need to create a file context_provider.go
in the catalog http
which will stop the server and cancel operations. The equivalent in .NET is CancellationToken.
package http
import (
"context"
"github.com/labstack/gommon/log"
"os"
"os/signal"
"syscall"
)
// NewContext - создать новый контекст приложения. Context - является аналогом CancellationToken
func NewContext() context.Context {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
go func() {
for {
select {
case <-ctx.Done():
log.Info("context is canceled!")
cancel()
return
}
}
}()
return ctx
}
How will it look like:
Adding application configuration
Next, to configure our service, we need to load the main settings from the configuration files in JSON format.
The library works with the Viper configuration, so let's first connect it to the project.
go get "github.com/spf13/viper"
Now we create a folder in the root of the project config
with files config.go
, config.development.json
Contents of config.go
package config
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/viper"
echoserver "go-template-microservice-v2/pkg/http/server"
"os"
)
type Config struct {
ServiceName string `mapstructure:"serviceName"`
Echo *echoserver.EchoConfig `mapstructure:"echo"`
}
func NewConfig() (*Config, *echoserver.EchoConfig, error) {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
cfg := &Config{}
viper.SetConfigName(fmt.Sprintf("config.%s", env))
viper.AddConfigPath("./config/")
viper.SetConfigType("json")
if err := viper.ReadInConfig(); err != nil {
return nil, nil, errors.Wrap(err, "viper.ReadInConfig")
}
if err := viper.Unmarshal(cfg); err != nil {
return nil, nil, errors.Wrap(err, "viper.Unmarshal")
}
return cfg, cfg.Echo, nil
}
Contents of config.development.json
{
"serviceName": "book_service",
"deliveryType": "http",
"context": {
"timeout": 20
},
"echo": {
"port": ":5000",
"development": true,
"timeout": 30,
"basePath": "/api/v1",
"host": "http://localhost",
"debugHeaders": true,
"httpClientDebug": true,
"debugErrorsResponse": true,
"ignoreLogUrls": [
"metrics"
]
}
}
Now we return to main.go to connect our configuration to DI:
package main
import (
"go-template-microservice-v2/config"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
echoserver.NewEchoServer,
),
),
).Run()
}
Server Tuning
Now, to implement the first part of our microservice, we need to configure a common server file in which echo_server will be launched and, in the future, a worker that will send new books from the warehouse via kafka.
So we go and create a directory in the root folder server
with file server.go
package server
import (
"context"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"go-template-microservice-v2/config"
echoserver "go-template-microservice-v2/pkg/http/server"
"go.uber.org/fx"
"log"
"net/http"
)
// RunServers - запустить все сервера
func RunServers(lc fx.Lifecycle, ctx context.Context, e *echo.Echo, cfg *config.Config) error {
lc.Append(fx.Hook{
OnStart: func(_ context.Context) error {
log.Println("Starting server")
// Запустить HTTP - сервер
go func() {
if err := echoserver.RunHttpServer(ctx, e, cfg.Echo); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error running http server: %v", err)
}
}()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, cfg.ServiceName)
})
return nil
},
OnStop: func(_ context.Context) error {
log.Println("all servers shutdown gracefully...")
return nil
},
})
return nil
}
Also for the future, install the package “github.com/go-playground/validator”
go get "github.com/go-playground/validator"
And we return to main.go to connect our server.
package main
import (
"github.com/go-playground/validator"
"go-template-microservice-v2/config"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go-template-microservice-v2/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
echoserver.NewEchoServer,
validator.New,
),
fx.Invoke(server.RunServers),
),
).Run()
}
Let's launch and check.
Our server has started, now we can start implementing our scheme, but first we will connect the database.
Database connection and configuration
First, connect the library for working with GUIDs and also download ORM gorm and additional drivers for connecting postgresql.
go get "github.com/satori/go.uuid"
go get "github.com/cenkalti/backoff/v4"
go get "github.com/uptrace/bun/driver/pgdriver"
go get "gorm.io/driver/postgres"
go get "gorm.io/gorm"
Let's go to the catalog pkg
in which we create a folder gorm_pg
since together with ORM we will use the postgresql database, and in this directory we create a file pg_gorm.go
The file itself with the connection settings and migration rollout will look like this.
package gormpg
import (
"database/sql"
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/pkg/errors"
"github.com/uptrace/bun/driver/pgdriver"
gorm_postgres "gorm.io/driver/postgres"
"gorm.io/gorm"
"time"
)
// PgConfig - конфигурация для соединения с Postgresql
type PgConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
DBName string `mapstructure:"dbName"`
SSLMode bool `mapstructure:"sslMode"`
Password string `mapstructure:"password"`
}
// PgGorm - модель базы данных
type PgGorm struct {
DB *gorm.DB
Config *PgConfig
}
func NewPgGorm(config *PgConfig) (*PgGorm, error) {
err := createDatabaseIfNotExists(config)
if err != nil {
panic(err)
return nil, err
}
connectionString := getConnectionString(config, config.DBName)
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 10 * time.Second
maxRetries := 5
var gormDb *gorm.DB
err = backoff.Retry(func() error {
gormDb, err = gorm.Open(gorm_postgres.Open(connectionString), &gorm.Config{})
if err != nil {
return errors.Errorf("failed to connect postgres: %v and connection information: %s", err, connectionString)
}
return nil
}, backoff.WithMaxRetries(bo, uint64(maxRetries-1)))
return &PgGorm{DB: gormDb, Config: config}, err
}
func Migrate(gorm *gorm.DB, types ...interface{}) error {
for _, t := range types {
err := gorm.AutoMigrate
if err != nil {
return err
}
}
return nil
}
func createDatabaseIfNotExists(config *PgConfig) error {
connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
config.User,
config.Password,
config.Host,
config.Port,
"postgres",
)
pgSqlDb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(connectionString)))
var exists int
selectDbQueryString := fmt.Sprintf("SELECT 1 FROM pg_catalog.pg_database WHERE datname="%s"", config.DBName)
rows, err := pgSqlDb.Query(selectDbQueryString)
if err != nil {
return err
}
if rows.Next() {
err = rows.Scan(&exists)
if err != nil {
return err
}
}
if exists == 1 {
return nil
}
createDbQueryString := fmt.Sprintf("CREATE DATABASE %s", config.DBName)
_, err = pgSqlDb.Exec(createDbQueryString)
if err != nil {
return err
}
defer func(pgSqlDb *sql.DB) {
err := pgSqlDb.Close()
if err != nil {
panic(err)
}
}(pgSqlDb)
return nil
}
func getConnectionString(config *PgConfig, dbName string) string {
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s",
config.Host,
config.Port,
config.User,
dbName,
config.Password,
)
}
Now let's correct our configuration files taking into account what has been added.
package config
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/viper"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
echoserver "go-template-microservice-v2/pkg/http/server"
"os"
)
type Config struct {
ServiceName string `mapstructure:"serviceName"`
Echo *echoserver.EchoConfig `mapstructure:"echo"`
PgConfig *gormpg.PgConfig `mapstructure:"pgConfig"`
}
func NewConfig() (*Config, *echoserver.EchoConfig, *gormpg.PgConfig, error) {
env := os.Getenv("APP_ENV")
if env == "" {
env = "development"
}
cfg := &Config{}
viper.SetConfigName(fmt.Sprintf("config.%s", env))
viper.AddConfigPath("./config/")
viper.SetConfigType("json")
if err := viper.ReadInConfig(); err != nil {
return nil, nil, nil, errors.Wrap(err, "viper.ReadInConfig")
}
if err := viper.Unmarshal(cfg); err != nil {
return nil, nil, nil, errors.Wrap(err, "viper.Unmarshal")
}
return cfg, cfg.Echo, cfg.PgConfig, nil
}
{
"serviceName": "book_service",
"deliveryType": "http",
"context": {
"timeout": 20
},
"echo": {
"port": ":5000",
"development": true,
"timeout": 30,
"basePath": "/api/v1",
"host": "http://localhost",
"debugHeaders": true,
"httpClientDebug": true,
"debugErrorsResponse": true,
"ignoreLogUrls": [
"metrics"
]
},
"PgConfig": {
"Host": "localhost",
"Port": 5432,
"User": "tgbotchecker",
"DbName": "tgbotchecker",
"SSLMode": false,
"Password": "tgbotchecker"
}
}
Next we connect dependencies in main.go
package main
import (
"github.com/go-playground/validator"
"go-template-microservice-v2/config"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go-template-microservice-v2/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
gormpg.NewPgGorm,
echoserver.NewEchoServer,
validator.New,
),
fx.Invoke(server.RunServers),
),
).Run()
}
To raise our database in Docker, we will create a folder in the root directory deployments
in which we will create docker-compose.yml
which we can call using the command in the console from the directory with the docker-compose up file.
version: "3.9"
services:
postgres:
image: postgres
environment:
POSTGRES_DB: "tgbotchecker"
POSTGRES_USER: "tgbotchecker"
POSTGRES_PASSWORD: "tgbotchecker"
ports:
- "5432:5432"
volumes:
- ./data:/var/lib/postgresql/data
Let's create DB entities and repositories
Let's go to the root directory and create a folder internal
in which we will create a directory data
and we will create a directory in it entities
and create a file in it book_entity.go
package entities
import (
uuid "github.com/satori/go.uuid"
)
// BookEntity model
type BookEntity struct {
Id uuid.UUID `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Author string `json:"author"`
Price float64 `json:"price"`
Enabled bool `json:"enabled"`
}
// CreateBookEntity создать модель
func CreateBookEntity(name string, author string, price float64) BookEntity {
return BookEntity{
Name: name,
Author: author,
Price: price,
Id: uuid.NewV4(),
Enabled: true,
}
}
Now, to create migrations in the main.go file, we need to register our entity.
package main
import (
"github.com/go-playground/validator"
"go-template-microservice-v2/config"
"go-template-microservice-v2/internal/data/entities"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go-template-microservice-v2/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
gormpg.NewPgGorm,
echoserver.NewEchoServer,
validator.New,
),
fx.Invoke(server.RunServers),
fx.Invoke(
func(sql *gormpg.PgGorm) error {
return gormpg.Migrate(sql.DB, &entities.BookEntity{})
}),
),
).Run()
}
Now in the folder data
let's create 2 directories contracts
And repositories
. In the catalog contracts
there will be an abstraction interface for our repository book_repository.go
and in the catalog repository
there will be a direct implementation for postgresql pg_book_repository.go
package contracts
import (
uuid "github.com/satori/go.uuid"
"go-template-microservice-v2/internal/data/entities"
)
type IBookRepository interface {
AddBook(bookEntity entities.BookEntity) error
GetBook(id uuid.UUID) (*entities.BookEntity, error)
GetAllBook() ([]*entities.BookEntity, error)
UpdateBook(bookEntity entities.BookEntity) error
}
package repositories
import (
"fmt"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"go-template-microservice-v2/internal/data/contracts"
"go-template-microservice-v2/internal/data/entities"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
)
type PgBookRepository struct {
PgGorm *gormpg.PgGorm
}
func NewPgBookRepository(pgGorm *gormpg.PgGorm) contracts.IBookRepository {
return &PgBookRepository{PgGorm: pgGorm}
}
func (p PgBookRepository) AddBook(bookEntity entities.BookEntity) error {
err := p.PgGorm.DB.Create(bookEntity).Error
if err != nil {
return errors.Wrap(err, "error in the inserting book into the database.")
}
return nil
}
func (p PgBookRepository) GetBook(id uuid.UUID) (*entities.BookEntity, error) {
var book entities.BookEntity
if err := p.PgGorm.DB.First(&book, id).Error; err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("can't find the book with id %s into the database.", id))
}
return &book, nil
}
func (p PgBookRepository) GetAllBook() ([]*entities.BookEntity, error) {
var books []*entities.BookEntity
if err := p.PgGorm.DB.Find(&books).Error; err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("can't find the books into the database."))
}
return books, nil
}
func (p PgBookRepository) UpdateBook(bookEntity entities.BookEntity) error {
err := p.PgGorm.DB.Save(bookEntity).Error
if err != nil {
return errors.Wrap(err, "error in the inserting book into the database.")
}
return nil
}
Now let's register our repository in DI in main.go
package main
import (
"github.com/go-playground/validator"
"go-template-microservice-v2/config"
"go-template-microservice-v2/internal/data/entities"
"go-template-microservice-v2/internal/data/repositories"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go-template-microservice-v2/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
gormpg.NewPgGorm,
repositories.NewPgBookRepository,
echoserver.NewEchoServer,
validator.New,
),
fx.Invoke(server.RunServers),
fx.Invoke(
func(sql *gormpg.PgGorm) error {
return gormpg.Migrate(sql.DB, &entities.BookEntity{})
}),
),
).Run()
}
Implementation of mediator commands
We are almost close to implementing the controller, there is only one last step left – to describe the commands that will play the role of services in our application.
go get "github.com/mehdihadeli/go-mediatr"
The commands in our application will also act as contracts for requests and responses.
For our implementation we will need the following commands: adding a new book, buying a book, and a request to get a list of all books.
So let's go to the catalog internal
and create a catalog features
in which we create 3 directories add_book
, buy_book
, get_all_books
.
First, we will work with the catalog add_book
. In which we will create a directory commands
in which we will create files: add_book_command.go
, add_book_handler.go
, add_book_response.go
.
package commands
// AddBookCommand - модель добавления книги в каталог
type AddBookCommand struct {
Name string `json:"name" validate:"required"`
Author string `json:"author" validate:"required"`
Price float64 `json:"price" validate:"required"`
}
package commands
import (
"context"
"go-template-microservice-v2/internal/data/contracts"
"go-template-microservice-v2/internal/data/entities"
)
// AddBookHandler - хендлер для команды AddUserRequestCommand
type AddBookHandler struct {
Repository contracts.IBookRepository
Ctx context.Context
}
// NewAddBookHandler - DI
func NewAddBookHandler(
repository contracts.IBookRepository,
ctx context.Context) *AddBookHandler {
return &AddBookHandler{Repository: repository, Ctx: ctx}
}
// Handle - выполнить
func (handler *AddBookHandler) Handle(ctx context.Context, command *AddBookCommand) (*AddBookResponse, error) {
bookEntity := entities.CreateBookEntity(
command.Name,
command.Author,
command.Price)
err := handler.Repository.AddBook(bookEntity)
if err != nil {
return nil, err
}
return &AddBookResponse{BookId: bookEntity.Id}, nil
}
package commands
import uuid "github.com/satori/go.uuid"
type AddBookResponse struct {
BookId uuid.UUID `json:"book_id"`
}
Now let's create an analogue of our controller in the catalog add_book
let's make a catalog endpoints
in which we will create a file add_book_endpoints
package endpoints
import (
"context"
"github.com/go-playground/validator"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/mehdihadeli/go-mediatr"
"github.com/pkg/errors"
"go-template-microservice-v2/internal/features/add_book/commands"
"net/http"
)
// MapRoute - настройка маршрутизации
func MapRoute(validator *validator.Validate, echo *echo.Echo, ctx context.Context) {
group := echo.Group("/api/v1/books")
group.POST("", addBook(validator, ctx))
}
// AddBook
// @Tags Book
// @Summary Add Book
// @Description Add new Book in catalogue
// @Accept json
// @Produce json
// @Param AddBookCommand body commands.AddBookCommand true "Book data"
// @Success 200 {object} commands.AddBookResponse
// @Security -
// @Router /api/v1/books [post]
func addBook(validator *validator.Validate, ctx context.Context) echo.HandlerFunc {
return func(c echo.Context) error {
request := &commands.AddBookCommand{}
if err := c.Bind(request); err != nil {
badRequestErr := errors.Wrap(err, "[addBookEndpoint_handler.Bind] error in the binding request")
log.Error(badRequestErr)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
if err := validator.StructCtx(ctx, request); err != nil {
validationErr := errors.Wrap(err, "[addBook_handler.StructCtx] command validation failed")
log.Error(validationErr)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
result, err := mediatr.Send[*commands.AddBookCommand, *commands.AddBookResponse](ctx, request)
if err != nil {
log.Errorf("(Handle) id: {%s}, err: {%v}", request.Name, err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
log.Infof("(auto added) id: {%s}", result.BookId)
return c.JSON(http.StatusCreated, result)
}
}
Let's move on to the next directory get_all_books
and create a directory in it queries
in which we create get_all_books_query
, get_all_books_handler
, get_all_books_response
package queries
type GetAllBooksQuery struct{}
package queries
import (
"context"
"go-template-microservice-v2/internal/data/contracts"
)
type GetAllBooksHandler struct {
Repository contracts.IBookRepository
Ctx context.Context
}
// NewGetAllBooksHandler - DI
func NewGetAllBooksHandler(
repository contracts.IBookRepository,
ctx context.Context) *GetAllBooksHandler {
return &GetAllBooksHandler{Repository: repository, Ctx: ctx}
}
// Handle - выполнить
func (handler *GetAllBooksHandler) Handle(ctx context.Context, command *GetAllBooksQuery) (*GetAllBooksResponse, error) {
getAllBooksResponse := &GetAllBooksResponse{
Books: make([]GetAllBooksResponseItem, 0),
}
result, err := handler.Repository.GetAllBook()
if err != nil {
return nil, err
}
for _, element := range result {
getAllBooksResponse.Books = append(getAllBooksResponse.Books, GetAllBooksResponseItem{
Id: element.Id,
Name: element.Name,
Author: element.Author,
Price: element.Price,
Enabled: element.Enabled,
})
}
return getAllBooksResponse, nil
}
package queries
import uuid "github.com/satori/go.uuid"
type GetAllBooksResponse struct {
Books []GetAllBooksResponseItem `json:"books,omitempty"`
}
type GetAllBooksResponseItem struct {
Id uuid.UUID `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
Price float64 `json:"price"`
Enabled bool `json:"enabled"`
}
And now, by analogy, we will create a catalog endpoints
with file get_all_books_endpoints.go
package endpoints
import (
"context"
"github.com/go-playground/validator"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/mehdihadeli/go-mediatr"
"go-template-microservice-v2/internal/features/get_all_books/queries"
"net/http"
)
// MapRoute - настройка маршрутизации
func MapRoute(validator *validator.Validate, echo *echo.Echo, ctx context.Context) {
group := echo.Group("/api/v1/books")
group.GET("", getAllBooks(validator, ctx))
}
// AddBook
// @Tags Book
// @Summary Get All Books
// @Description Get All Books from catalogue
// @Accept json
// @Produce json
// @Param GetAllBooksQuery body queries.GetAllBooksQuery true "Book data"
// @Success 200 {object} queries.GetAllBooksResponse
// @Security -
// @Router /api/v1/books [get]
func getAllBooks(validator *validator.Validate, ctx context.Context) echo.HandlerFunc {
return func(c echo.Context) error {
query := queries.GetAllBooksQuery{}
result, err := mediatr.Send[*queries.GetAllBooksQuery, *queries.GetAllBooksResponse](ctx, &query)
if err != nil {
log.Errorf("(Handle) err: {%v}", err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
return c.JSON(http.StatusCreated, result)
}
}
All that remains is to implement the last command and we can begin registering mediators.
Let's move on to the next directory buy_book
and create a directory in it commands
in which we create buy_book_commands
, buy_book_handler
, buy_book_response.
package commands
import uuid "github.com/satori/go.uuid"
// BuyBookCommand - модель добавления книги в каталог
type BuyBookCommand struct {
BookId uuid.UUID `json:"BookId" validate:"required"`
}
package commands
import (
"context"
"go-template-microservice-v2/internal/data/contracts"
)
// BuyBookHandler - хендлер для команды AddUserRequestCommand
type BuyBookHandler struct {
Repository contracts.IBookRepository
Ctx context.Context
}
// NewBuyBookHandler - DI
func NewBuyBookHandler(
repository contracts.IBookRepository,
ctx context.Context) *BuyBookHandler {
return &BuyBookHandler{Repository: repository, Ctx: ctx}
}
// Handle - выполнить
func (handler *BuyBookHandler) Handle(ctx context.Context, command *BuyBookCommand) (*BuyBookResponse, error) {
book, err := handler.Repository.GetBook(command.BookId)
if err != nil {
return nil, err
}
book.Enabled = false
err = handler.Repository.UpdateBook(*book)
if err != nil {
return nil, err
}
return &BuyBookResponse{Result: book.Enabled}, nil
}
package commands
type BuyBookResponse struct {
Result bool `json:"result"`
}
package endpoints
import (
"context"
"github.com/go-playground/validator"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
"github.com/mehdihadeli/go-mediatr"
"github.com/pkg/errors"
"go-template-microservice-v2/internal/features/buy_book/commands"
"net/http"
)
// MapRoute - настройка маршрутизации
func MapRoute(validator *validator.Validate, echo *echo.Echo, ctx context.Context) {
group := echo.Group("/api/v1/books/buy")
group.POST("", buyBook(validator, ctx))
}
// AddBook
// @Tags Book
// @Summary Buy Book
// @Description Buy Book in catalogue
// @Accept json
// @Produce json
// @Param BuyBookCommand body commands.BuyBookCommand true "Book data"
// @Success 200 {object} commands.BuyBookResponse
// @Security -
// @Router /api/v1/books/buy [post]
func buyBook(validator *validator.Validate, ctx context.Context) echo.HandlerFunc {
return func(c echo.Context) error {
request := &commands.BuyBookCommand{}
if err := c.Bind(request); err != nil {
badRequestErr := errors.Wrap(err, "[addBookEndpoint_handler.Bind] error in the binding request")
log.Error(badRequestErr)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
if err := validator.StructCtx(ctx, request); err != nil {
validationErr := errors.Wrap(err, "[addBook_handler.StructCtx] command validation failed")
log.Error(validationErr)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
result, err := mediatr.Send[*commands.BuyBookCommand, *commands.BuyBookResponse](ctx, request)
if err != nil {
log.Errorf("(Handle) err: {%v}", err)
return echo.NewHTTPError(http.StatusBadRequest, err)
}
log.Infof("(auto added) id: {%s}", result.Result)
return c.JSON(http.StatusCreated, result)
}
}
Registration of routes and mediator commands.
To register routing and commands, the mediator must be in the directory internal
create a directory configurations
in which create 2 files endpoints_configurations
And mediator_configurations
.
package configurations
import (
"context"
"github.com/go-playground/validator"
"github.com/labstack/echo/v4"
addBookEndpoints "go-template-microservice-v2/internal/features/add_book/endpoints"
buyBookEndpoints "go-template-microservice-v2/internal/features/buy_book/endpoints"
getAllBooksEndpoints "go-template-microservice-v2/internal/features/get_all_books/endpoints"
)
// ConfigEndpoints - конфигурирование ендпоинтов нашего API
func ConfigEndpoints(validator *validator.Validate, echo *echo.Echo, ctx context.Context) {
addBookEndpoints.MapRoute(validator, echo, ctx)
buyBookEndpoints.MapRoute(validator, echo, ctx)
getAllBooksEndpoints.MapRoute(validator, echo, ctx)
}
package configurations
import (
"context"
"github.com/mehdihadeli/go-mediatr"
"go-template-microservice-v2/internal/data/contracts"
addBookCommand "go-template-microservice-v2/internal/features/add_book/commands"
buyBookCommand "go-template-microservice-v2/internal/features/buy_book/commands"
getAllBooksQueries "go-template-microservice-v2/internal/features/get_all_books/queries"
)
// ConfigMediator - DI
func ConfigMediator(
ctx context.Context,
repository contracts.IBookRepository) (err error) {
err = mediatr.RegisterRequestHandler[
*addBookCommand.AddBookCommand,
*addBookCommand.AddBookResponse](addBookCommand.NewAddBookHandler(repository, ctx))
err = mediatr.RegisterRequestHandler[
*buyBookCommand.BuyBookCommand,
*buyBookCommand.BuyBookResponse](buyBookCommand.NewBuyBookHandler(repository, ctx))
err = mediatr.RegisterRequestHandler[
*getAllBooksQueries.GetAllBooksQuery,
*getAllBooksQueries.GetAllBooksResponse](getAllBooksQueries.NewGetAllBooksHandler(repository, ctx))
if err != nil {
return err
}
return nil
}
Now all that remains is to register all this in DI in the main.go file.
package main
import (
"github.com/go-playground/validator"
"go-template-microservice-v2/config"
"go-template-microservice-v2/internal/configurations"
"go-template-microservice-v2/internal/data/entities"
"go-template-microservice-v2/internal/data/repositories"
gormpg "go-template-microservice-v2/pkg/gorm_pg"
"go-template-microservice-v2/pkg/http"
echoserver "go-template-microservice-v2/pkg/http/server"
"go-template-microservice-v2/server"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Options(
fx.Provide(
config.NewConfig,
http.NewContext,
gormpg.NewPgGorm,
repositories.NewPgBookRepository,
echoserver.NewEchoServer,
validator.New,
),
fx.Invoke(configurations.ConfigEndpoints),
fx.Invoke(configurations.ConfigMediator),
fx.Invoke(server.RunServers),
fx.Invoke(
func(sql *gormpg.PgGorm) error {
return gormpg.Migrate(sql.DB, &entities.BookEntity{})
}),
),
).Run()
}
Let's check that everything is up and running
We check in the database that the migration table has been created
Checking the request
Checking price validation
Checking if the book has been added
Great, we have a working microservice!
Conclusion
In this article, I decided to share and introduce the structure of the microservice project, where we had validation of input requests, contracts were replaced with commands and mediator requests, a database was connected, DI and configuration were set up, and endpoints were also set up as analogs of controllers.
Thanks for your attention! In the next article I plan to connect kafka and start writing component tests for this functionality, as well as connect swagger.
If you liked it, here is mine telegram channelThere I share article announcements, share my thoughts on various points in IT, and also talk about my study of Golang.
Repository link: https://github.com/ItWithMisha/go-template-microservice-v2