Go lang: From 0 to Employed
Go lang foundations :: Clean Architecture :: part 2
If you skipped part1 go back here!
Golang Clean Architecture tutorial
Looking to conquer Clean Architecture with Golang? This guide’s got your back. Embark on a coding journey with this compact guide to learn Clean Architecture with Gorilla Mux! You’ll go from project setup to server deployment, learning key concepts such as dependency injection and Clean Architecture. Expect hands-on work with entities, use cases, testing, and building RESTful API endpoints.
By the end, you’ll be equipped with skills to develop, test, and deploy resilient Golang servers. So, ready to transform your coding journey and fast-track your way to Golang proficiency? Let’s get started and make coding magic happen!
Attention: For who is this guide?
This guide is part of a web series intended for individuals who are already proficient in a programming language and are looking to learn GoLang in a simple and fast manner. Therefore, I cover various aspects of the language directly and succinctly, aiming to provide the necessary material for a smooth career transition from other languages to Go. The focus is on supplying ample learning material and support, enabling developers unfamiliar with Go to start working with GoLang as quickly as possible.
Index:
- Introduction to Clean Architecture and Gorilla Mux
- Setting up the development environment
- Creating the project structure
— Folder structure
— Organizing the application in layers - Understanding dependency injection in Clean Architecture
— Separation of concerns
— Inversion of control
— Benefits of dependency injection in Clean Architecture - Defining entities and use cases
— Entities
— Use Cases - Implementing the repository and service
— Repository
— Service - Implementing the presentation layer
— Presenting Gorilla Mux
— Installing Gorilla Mux
— Define routes and HTTP methods
— Create a handler struct
— Create a server Router with the Gorilla Mux router
— Middlewares
— Handling input validation and error handling
— Starting the server - Writing tests for the Golang server
— Unit testing
— Integration testing the API endpoints
— Best practices for testing Clean Architecture applications - Deploying the Golang server
— Overview
— Preparing the application for deployment
— Monitoring and maintaining the application
Feeling lost? Fear not, adventurer! Follow this link for a fresh restart. Your journey starts here!
Implementing the presentation layer
The presentation layer in Clean Architecture is responsible for handling user interactions, managing input and output, and providing a clear separation of concerns between the UI and the core business logic. In our Golang server application, the presentation layer consists of HTTP handlers and the router configuration using Gorilla Mux.
In this section, we’ll discuss how to implement the presentation layer by creating HTTP handlers, organizing them in a struct, and integrating them with the Gorilla Mux router.
Presenting Gorilla Mux
Gorilla Mux is a powerful URL router and dispatcher library for Golang that makes it easy to build scalable and maintainable RESTful APIs. In this step, we’ll configure Gorilla Mux to handle the routing of our server application.
Installing Gorilla Mux
Before you can use Gorilla Mux in your Golang server application, you need to install the library. Gorilla Mux is a powerful URL router and dispatcher library for Golang that allows you to build scalable and maintainable RESTful APIs.
To install Gorilla Mux, open your terminal and run the following command:
go get -u github.com/gorilla/mux
This command fetches the latest version of Gorilla Mux and installs it in your GOPATH. Once the installation is complete, you can import and use Gorilla Mux in your Golang server application to configure routing and request handling.
With Gorilla Mux installed, you can now proceed to configure and set up the router, define routes, and create handler functions for your Golang server application while adhering to the Clean Architecture principles.
Building RESTful API endpoints
In the presentation layer, one common way to handle user interactions is by building RESTful API endpoints. These endpoints allow client applications to interact with your Golang server using HTTP methods and standardized URL patterns. In this section, we’ll discuss how to build RESTful API endpoints using Gorilla Mux and Clean Architecture principles.
After installing Gorilla Mux, you can set up routes and middleware for your Golang server application. Routes define the URL patterns and the corresponding handler functions for different HTTP methods, while middleware allows you to process requests and responses at different stages in the request-response cycle.
Define routes and HTTP methods
Design thoughtful routes for your application. They need not correspond to each resource, or always follow CRUD conventions. Incorporate various HTTP methods, allowing flexibility in your microservices architecture and embracing RESTful principles.
Define routes using standardized URL patterns and associate them with the appropriate HTTP methods, such as GET, POST, PUT, and DELETE. This will allow your API to follow RESTful design principles.
For example, you might define the following routes for a blog server:
- GET /posts: Retrieve a list of posts
- POST /posts: Create a new post
- GET /posts/{id}: Retrieve a specific post by ID
- PUT /posts/{id}: Update a specific post by ID
- DELETE /posts/{id}: Delete a specific post by ID
For each route, create a corresponding handler function in the appropriate handler struct. These functions should process the incoming HTTP request, call the corresponding use case, and return the appropriate HTTP response.
Create a handler struct
In the presentation/api directory, create a new file named post_handler.go. In this file, define a struct called PostHandler that includes the PostUseCase interface from your domain layer. This struct will contain the methods for handling HTTP requests related to posts.
Mapping use cases to API handlers is an essential part of implementing the presentation layer in a Clean Architecture-based Golang server application. This process involves connecting the business logic (use cases) with the HTTP handlers responsible for processing client requests and generating responses. In this section, we’ll discuss how to map use cases to API handlers.
Step 1: Define use case interfaces
In the domain layer, define interfaces that describe the use cases for each resource in your application (e.g., PostUseCase, UserUseCase). These interfaces should include the methods required to perform the various operations on the resource, such as creating, reading, updating, and deleting.
// domain/post_usecase.go
package domain
type PostUseCase interface {
Create(post *Post) error
GetPosts() ([]Post, error)
// Add other methods as needed
}
Step 2: Implement use cases in the service layer
In the service layer, create a struct that implements the use case interface, and provide concrete implementations for each method. These implementations should contain the core business logic of your application, interacting with repositories to persist and retrieve data.
// service/post_service.go
package service
type postService struct {
repo domain.PostRepository
}
func NewPostService(repo domain.PostRepository) domain.PostUseCase {
return &postService{repo: repo}
}
func (s *postService) Create(post *domain.Post) error {
// Implement the business logic for creating a post
}
Step 3: Inject use cases into handler structs
In the presentation layer, create handler structs for each route and include the corresponding use case interfaces as fields. Then, inject the appropriate use case implementation when creating a new instance of the handler struct.
// presentation/api/post_handler.go
package api
type PostHandler struct {
useCase domain.PostUseCase
}
func NewPostHandler(useCase domain.PostUseCase) *PostHandler {
return &PostHandler{useCase: useCase}
}
step 4: Call use cases in handler functions
In the handler functions, manage the incoming HTTP request, invoke the relevant use case method, and yield the suitable HTTP response. These methods, which should be invoked on the use case interface field in the handler struct, are designed to accept the incoming HTTP request, extract the necessary data, call the relevant use case, and deliver the appropriate HTTP response.”
For example, you might implement a Create method to handle post creation requests:
// presentation/api/post_handler.go
package api
type PostHandler struct {
useCase domain.PostUseCase
}
func NewPostHandler(useCase domain.PostUseCase) *PostHandler {
return &PostHandler{useCase: useCase}
}
func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
var post domain.Post
err := json.NewDecoder(r.Body).Decode(&post)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.useCase.Create(&post)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(post)
}
By mapping use cases to API handlers, you can maintain a clear separation of concerns between your business logic and presentation layer, ensuring that your Golang server application adheres to the principles of Clean Architecture.
Create a server Router with the Gorilla Mux router
Step 1: Define the Router structure
The first step is to define a Router struct that will hold the Gorilla Mux router and use case dependencies.
type Router struct {
router *mux.Router
useCases UseCases
//Add whatever you need like logger, tracer, metrics or any other dependencies
}
Router struct includes a router field of type *mux.Router and a UseCases field which will contain all the use cases that are defined in the domain.
Step 2: Define UseCases struct
We also define another struct UseCases to hold all the use cases required in our application. For instance, if we have a PostUseCase in our domain, we include it in our UseCases struct.
type UseCases struct {
postUseCase domain.PostUseCase
// Define other use cases as needed
}
The UseCases struct is essentially acting as a container or aggregator for all of your use case objects that come from the domain layer. It’s essentially a part of the infrastructure layer because it’s concerned with how the application is wired together, rather than encapsulating any business logic itself.
That being said, the specific location of this struct within your project structure can depend on the organization of your codebase. It is generally put where it best suits the design and organization of your application. You could have a separate package where you wire up all of your dependencies (sometimes called a config or di (for Dependency Injection) package) where the UseCases struct could live. This way you can keep all your dependency wiring separate from your actual business logic and handlers.
In the end, the guiding principle should be the separation of concerns. The UseCases struct is part of setting up your application structure and wiring up dependencies, so it should be placed somewhere that aligns with that responsibility.
Step 3: Implement the NewRouter function
Now, we need to define a function that will initialize our Router struct.
func NewRouter(useCases UseCases) *Router {
router := mux.NewRouter()
myRouter := &Router{router: router, useCases: useCases}
// Call configRoutes to register routes with handlers
myRouter.configRoutes()
// Pass other handlers to configRoutes as needed
return myRouter
}
In NewRouter, we create an instance of mux.Router and an instance of our Router struct. Then, we initialize our handler with the respective use case from the UseCases struct, call configRoutes method to associate the routes with the respective handlers, and return the Router instance.
Step 4: Implement the configRoutes method
This is the method that will map the routes to their respective handler methods.
func (router *Router) configRoutes() {
postHandler := NewPostHandler(router.useCases.PostUseCase)
router.route.HandleFunc("/posts", postHandler.GetPosts).Methods("GET")
// Register other routes and handlers as needed
}
The configRoutes method is defined on the Router struct and it takes the handler as an argument. Inside, we call HandleFunc on r.router to register the route and associate it with the handler method.
Step 4: Implement the Shutdown method
func (r *Router) Shutdown(ctx context.Context) error {
// Implement shutdown procedure
}
In this method, you would typically call a shutdown method on the underlying *mux.Router or other cleanup tasks as needed.
By following this pattern, you create a well-structured, easily maintainable Golang server application that also follows best practices for handling user interactions and adheres to the principles of Clean Architecture.
Once you’ve implemented the handler functions, register them with the Gorilla Mux router. In your router.go file, use the HandleFunc method to associate each route with the corresponding handler function and specify the HTTP method.
By building RESTful API endpoints using Gorilla Mux and Clean Architecture principles, you can create a well-structured and easily maintainable Golang server application that follows best practices for handling user interactions and adheres to the principles of Clean Architecture.
Middlewares
Implement middleware
Middleware functions allow you to perform actions before or after the request is processed by the handler. Middleware can be used for tasks like logging, authentication, or adding response headers.
To create a middleware function, define a function with the following signature:
func myMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing actions
next.ServeHTTP(w, r)
// Post-processing actions
})
}
Replace myMiddleware with a descriptive name for your middleware, such as loggingMiddleware or authenticationMiddleware.
Register middleware with the router
To register your middleware with the Gorilla Mux router, use the Use method:
router.Use(myMiddleware)
The middleware will be executed in the order they are added. You can register multiple middleware functions by chaining the Use method:
router.Use(middleware1).Use(middleware2).Use(middleware3)
By setting up routes and middleware using Gorilla Mux, you can effectively manage the request-response cycle, handle different URL patterns, and implement features like logging or authentication in a modular way while adhering to Clean Architecture principles.
Handling input validation and error handling
Proper input validation and error handling are crucial aspects of building a robust and secure Golang server application. When implementing the presentation layer with Clean Architecture, you should handle input validation and error handling within the HTTP handler functions. In this section, we’ll discuss how to handle input validation and error handling effectively.
Step 1: Validate incoming data
When processing an incoming HTTP request, ensure that the data provided by the client is valid and well-formed. Use the encoding/json package to decode JSON data into a struct and perform validation checks on the struct fields.
func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
var post domain.Post
err := json.NewDecoder(r.Body).Decode(&post)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Perform validation checks on the 'post' struct
// …
}
You can create custom validation functions or use third-party libraries, like govalidator, to simplify the validation process.
Step 2: Handle domain errors
To follow the RFC 7807 standard for API problem details, we can define a DomainError struct and have it implement the error interface:
type DomainError struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
}
// Error implements the error interface.
// It returns a string representation of the error,
// which in this case is the Title field.
func (d *DomainError) Error() string {
return fmt.Sprintf("%s: %s", d.Title, d.Detail)
}
In this DomainError struct, we have four fields:
- Type: A URI reference that identifies the problem type. This URI should ideally provide human-readable documentation for the problem type (as per RFC 7807).
- Title: A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem, except for purposes of localization.
- Status: The HTTP status code generated by the origin server for this occurrence of the problem.
- Detail: A human-readable explanation specific to this occurrence of the problem.
The Error method on the DomainError struct simply returns the Title, which is a human-readable summary of the problem. This allows the DomainError to be used anywhere an error is expected. You can, of course, customize this method to return a different string or a combination of different fields.
The DomainError struct is also annotated with json tags, which means when instances of DomainError are encoded to JSON (for example, to be sent as a response body from an HTTP server), the resulting JSON keys will be in the specified format (type, title, status, detail), following the RFC 7807 standard.
When calling use cases from your handler functions, handle any errors returned by the use case methods. These errors might include issues related to data persistence, business rule violations, or external service failures.
func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
// …
err = h.useCase.Create(&post)
if err != nil {
// Handle domain-specific errors
// …
}
// …
}
Step 3: Return appropriate HTTP status codes and error messages
Based on the type of error encountered, return an appropriate HTTP status code and error message to the client. Use the http.Error function to send an error response with a specific status code.
func (h *PostHandler) Create(w http.ResponseWriter, r *http.Request) {
var post domain.Post
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&post); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
err := h.useCase.Create(&post)
if err != nil {
switch err {
case ErrInvalidPostData:
http.Error(w, err.Error(), http.StatusBadRequest)
case ErrPostAlreadyExists:
http.Error(w, err.Error(), http.StatusConflict)
default:
log.Println(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(post)
}
By handling input validation and error handling effectively within the HTTP handler functions, you can ensure that your Golang server application built with Clean Architecture principles remains robust, secure, and capable of providing meaningful feedback to clients.
Starting the server
Create a main.go file in cmd package
In your project directory, create a cmd folder if it does not already exist. Inside cmd, create a new file and name it main.go. This will serve as your program’s entry point.
In the main.go file, you will define your main() function, which is the entry point of your Go application.
package main
import (
"log"
"net/http"
// Import other necessary packages
)
func main() {
// Initialization steps as before…
// Create an instance of the router
router := infrastructure.NewRouter(useCases)
// Configure your API routes
router.configRoutes()
// Start the server
log.Println("Starting server on port 8080")
err := http.ListenAndServe(":8080", router.router)
if err != nil {
log.Fatalf("Could not start server: %v\n", err)
}
}
Finally, you will start the HTTP server. The server will listen and serve on a specified port.
Your adventure isn’t over yet! Embark on Part 3, dive into new knowledge right here!