Hello Hi! ๐
I'm back after my exams ๐ฅต and ready to continue my back-end developer journey ๐ซก๐ฏ
Introduction
In my previous blogs, we saw how we can build a web server using Go and Gin. We also saw the theoretical concepts of Clean Architecture. So how about we apply the architecture to our server now?
Sounds interesting? Let's go!
The steps we will follow to construct the layers will be as follows:
Construct the
entities
layerConstruct the
UseCase
layer by defining an interface, followed by its concrete implementationConstruct the
database
interface, and its concrete implementationSet up the
router
to handle requestsConnect the different layers in the
main.go
file
Setting Up the Layers
Entities
In the server we made in my first blog, we had only one entity: Person
Hence, our entity layer would have a single struct (I'm replacing person
with User
and people
with Users
).
The entities/user.go
file will contain:
package entities
// data definitions
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
(The final file/folder structure, and the GitHub link for the complete project, are at the end of the blog for reference)
Usecase
Steps for constructing this layer:
Make a
UserUseCase
interfaceAdd the function skeletons for user actions within the interface
Build the concrete implementation within
usecases/user/user.go
// usecases/usecases.go
// this contains all the interfaces for all usecasses
// (currently, only UserUseCase)
package usecases
import "web-server/entities"
type UserUseCase interface {
GetAll() ([]*entities.User, error)
GetByID(id string) (*entities.User, error)
CreateNew(user *entities.User) error
}
// usecases/user/user.go
package usecases
import (
"web-server/database"
"web-server/entities"
)
// userUseCase struct contains a UserDB instance
type userUseCase struct {
DB database.UserDB
}
// return UseCase object to be passed on to the Router
func NewUserUseCase(userRepo database.UserDB) *userUseCase {
return &userUseCase{
DB: userRepo,
}
}
func (uc *userUseCase) GetAll() ([]*entities.User, error) {
return uc.DB.GetAll()
}
func (uc *userUseCase) GetByID(id string) (*entities.User, error) {
return uc.DB.GetByID(id)
}
func (uc *userUseCase) CreateNew(user *entities.User) error {
return uc.DB.CreateNew(user)
}
Database
First, we will make a UserDB
interface and write the skeletons for all the functions we need.
// database/database.go
package database
import "web-server/entities"
type UserDB interface {
GetAll() ([]*entities.User, error)
GetByID(id string) (*entities.User, error)
CreateNew(user *entities.User) error
}
Now for concrete implementations:
// database/users/users.go
package database
import "web-server/entities"
type UserDB struct {
users []*entities.User
}
func NewUserDB() *UserDB {
return &UserDB{
users: []*entities.User{
{
ID: "1",
Name: "ABC",
},
{
ID: "2",
Name: "DEF",
},
{
ID: "3",
Name: "GHI",
},
},
}
}
func (repo *UserDB) GetAll() ([]*entities.User, error) {
return repo.users, nil
}
func (repo *UserDB) GetByID(id string) (*entities.User, error) {
for _, user := range repo.users {
if user.ID == id {
return user, nil
}
}
return nil, nil // or an error indicating user not found
}
func (repo *UserDB) CreateNew(user *entities.User) error {
repo.users = append(repo.users, user)
return nil
}
Router Setup
Now, we will make the user router, which will handle all the requests coming to /users
// routes/users/users.go
package routes
import (
"net/http"
"web-server/entities"
"web-server/usecases"
"github.com/gin-gonic/gin"
)
func SetupRouter(userUC usecases.UserUseCase) *gin.Engine {
router := gin.Default()
// GET All Users
router.GET("/users", func(c *gin.Context) {
users, err := userUC.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users"})
return
}
c.IndentedJSON(http.StatusOK, users)
})
// GET User by ID
router.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
user, err := userUC.GetByID(id)
if err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
return
}
if user == nil {
c.IndentedJSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.IndentedJSON(http.StatusOK, user)
})
// POST Create New User
router.POST("/users", func(c *gin.Context) {
var user entities.User
if err := c.ShouldBindJSON(&user); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"})
return
}
if err := userUC.CreateNew(&user); err != nil {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
c.Status(http.StatusCreated)
})
return router
}
Assembling the Pieces
Finally, we will make the main.go
file to string together all the different layers.
package main
import (
userDatabase "web-server/database/users"
userRouter "web-server/routes/users"
userUseCase "web-server/usecases/user"
)
func main() {
// Initialize repository
userDB := userDatabase.NewUserDB()
// Initialize use case
userUC := userUseCase.NewUserUseCase(userDB)
// Set up router
router := userRouter.SetupRouter(userUC)
// Run the server
router.Run(":8080")
}
Here, we can see we have "injected" the dependencies when calling the constructor so that if we swap out the DB layer to let's say a Postgres instance, then we can easily do so within just the database layer (or ./database
) and the new DB implementation will then be passed on to the use case layer, and since the enforced interface is still valid, no changes should be required (if the architecture implementation is perfect ๐
)
In the current state, some of the steps might seem redundant, but the goal was to attempt at replicating how we would make large scale projects.
File Structure
The overall file structure of the project is:
web-server
โโโ database
โ โโโ users
โ โ โโโ users.go
โ โโโ database.go
โโโ entities
โ โโโ user.go
โโโ routes
โ โโโ users
โ โโโ users.go
โโโ usecases
โ โโโ user
โ โ โโโ user.go
โ โโโ usecases.go
โโโ go.mod
โโโ go.sum
โโโ main.go
8 directories, 9 files
Afterword
The End! ๐
I was planning on connecting PostgreSQL to the server instead of an in-memory DB within the same blog but then decided to split it into two.
So stay tuned! ๐
References
The full code with a few tweaks, some more functions, etc. is uploaded on my GitHub:
The previous blogs mentioned: