So far it's been going pretty well until we realized that dependency injection is still not standardized and each layer is too easy to access to potentially become tightly coupled. To keep each layer have clear boundaries, allowing replacement of parts without affecting the rest of the project and easier to build, test and maintain, it is necessary to standardize how dependencies are injected.
The design I propose is as follows:
use_case.go
package user
import (
"net/url"
"{module}/app"
)
type UseCase struct {
// Dependencies (interface or func or struct or whatever as required)
DB app.DB
Cache app.Cache
Validator app.Validator
User app.User
Query url.Values
...
}
func (uc *UseCase) GetByID(id string) (User, error) {
// pure business logic without direct call to any deps
...
return User{}, nil
}
func (uc *UseCase) Get() (UserList, error) {
// pure business logic without direct call to any deps
...
return UserList{}, nil
}
func (uc *UseCase) Create(param *CreateUserParam) error {
// pure business logic without direct call to any deps
...
return nil
}
func (uc *UseCase) UpdateByID(id string, param *UpdateUserParam) error {
// pure business logic without direct call to any deps
...
return nil
}
func (uc *UseCase) PartialyUpdateByID(id string, param *PartialyUpdateUserParam) error {
// pure business logic without direct call to any deps
...
return nil
}
func (uc *UseCase) DeleteByID(id string, param *PartialyUpdateUserParam) error {
// pure business logic without direct call to any deps
...
return nil
}
rest_api_handler.go (or grpc_handler, etc)
package user
import (
"github.com/gofiber/fiber/v2"
"{module}/app"
)
type RESTfulAPI struct {
UseCase *UseCase
}
// dependency injection
func (r *RESTfulAPI) useCaseImpl(c *fiber.Ctx) error {
// inject the dependency here
return nil
}
// Get user by id
// GET /users/:id
func (r *RESTfulAPI) GetByID(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
res, err := r.UseCase.GetByID(c.Params("id"))
if err != nil {
return app.ErrorHandler(c, err)
}
return c.JSON(grest.NewJSON(res).ToStructured().Data)
}
// Get list of user
// GET /users
func (r *RESTfulAPI) Get(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
res, err := r.UseCase.Get()
if err != nil {
return app.ErrorHandler(c, err)
}
return c.JSON(grest.NewJSON(res).ToStructured().Data)
}
// Create user
// POST /users
func (r *RESTfulAPI) Create(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
param := CreateUserParam{}
err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
err = r.UseCase.Create(¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
res, err := r.UseCase.GetByID(param.ID)
if err != nil {
return app.ErrorHandler(c, err)
}
return c.Status(http.StatusCreated).JSON(grest.NewJSON(res).ToStructured().Data)
}
// Update user by id
// PUT /users/:id
func (r *RESTfulAPI) UpdateByID(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
param := UpdateUserParam{}
err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
err = r.UseCase.UpdateByID(c.Params("id"), ¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
res, err := r.UseCase.GetByID(param.ID)
if err != nil {
return app.ErrorHandler(c, err)
}
return c.JSON(grest.NewJSON(res).ToStructured().Data)
}
// Partialy update user by id
// PATCH /users/:id
func (r *RESTfulAPI) PartialyUpdateByID(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
param := PartialyUpdateUserParam{}
err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
err = r.UseCase.PartialyUpdateByID(c.Params("id"), ¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
res, err := r.UseCase.GetByID(param.ID)
if err != nil {
return app.ErrorHandler(c, err)
}
return c.JSON(grest.NewJSON(res).ToStructured().Data)
}
// Delete user by id
// DELETE /users/:id
func (r *RESTfulAPI) DeleteByID(c *fiber.Ctx) error {
err := r.useCaseImpl(c)
if err != nil {
return app.ErrorHandler(c, err)
}
param := DeleteUserParam{}
err = grest.NewJSON(c.Body()).ToFlat().Unmarshal(¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
err = r.UseCase.DeleteByID(c.Params("id"), ¶m)
if err != nil {
return app.ErrorHandler(c, err)
}
return c.JSON(app.GeneralResponse{Code: 200, Message: r.UseCase.Trans(r.UseCase.Lang, "deleted")})
}
To keep it simple, the current rest api application project structure proposed by GREST is as follows:
So far it's been going pretty well until we realized that dependency injection is still not standardized and each layer is too easy to access to potentially become tightly coupled. To keep each layer have clear boundaries, allowing replacement of parts without affecting the rest of the project and easier to build, test and maintain, it is necessary to standardize how dependencies are injected.
The design I propose is as follows: use_case.go
rest_api_handler.go (or grpc_handler, etc)