Let’s Build a Movie API With Separated Layered Architecture Using Go
The goal of software architecture is to minimize the human resources required to build and maintain the required system. [1]
In my first article, we talked about testing with Go. Now, we will move forward to see how we can test our movie API more easily thanks to the three-layer architecture.
The idea of a layered architecture is built on the idea of programming to interfaces. When one module interacts with another though an interface, you can substitute one service provider for another [2].
Don’t worry! I will do my best to explain simply.
Let’s look at this diagram below. Look simple, right?
The logic behind this architecture every single layer has its own responsibility and they will be independent of each other. In that way, we can test them in a more isolable way. Yeap, again testing!
What are the roles of these layers (handler, service, and repository)❓
Handler: It is a layer which gets http request and returns http response to the client.
Service: It is a layer where our business logic is in.
Repository: It is a layer which provides all necessary data from external (DBs) or internal (in-memory) data source. For simplicity, we use in-memory.
We need to talk about Dependency injection before learning the role of interfaces in a diagram. Dependency injection is based on only abstractions (interfaces) not concrete types (structs). In that manner, we inject all our dependencies by using interfaces.
For example, we will have two services in the project: MockService
and DefaultService.
With the help of the service interface, we can use the methods of these two different structs in the Handler layer. In testing stage, Handler interact with MockService
, on the other hand, it interact with DefaultService
in production.
Don’t worry, you will understand better when we’ll see the action:
Actually, when we call the service method in Handler and the repository method in Service, its reasonable to think of this relations as “call stack”.
This is the simple diagram that we did with the call stack. Our computer allocates memory for our function call.
Let’s imagine this memory as a box.
Our first call(in Handler
) needs to be saved in memory.
Then, our second call(in Service
) is saved in memory upon the Handler box.
Then, our third call(in Repository
) is saved in memory upon Service box. The tricky is here is that our service needs to wait for the repository function to be done. When it is done, our handler needs to wait for the service function to be done. When it is done, the Handler
function can do its work.
After it is done too, our stack will be empty. This is how the call stack works. You can read the ‘Grokking Algorithms Book’s Call Stack section to learn more.
This is the broad perspective of how our design is created. We can go deep into our project. We will handle just an example of PATCH request along with this article.
1. main.go
When the client send request with PATCH method, movieHandler’s UpdateMovie method is called using httprouter
.
2a. Handler
This is the first layer where our API gets HTTP request.
Handler is the layer which transform response coming from service to http response. As you know, HTTP response is constituted of a status code, header and body.
2b. Handler Testing
Before continuing testing of the handler, I want to introduce mockgen package. This package helps our code to test easily.
Give it the interface source path which you want to mock, show it where the auto-generated mock file will be created and let it generate mock struct implementation on behalf of you. If we want to test the handler layer, we need to mock the service layer. We can do that install it like this:
mockgen -source service/movie_service_interface.go -destination service/mock_movie_service.go -package service
In the test file, we should test possible errors and success cases to increase the test coverage of the handler.
3a. Service
This layer includes our app business logic. By separating business logic into a specific layer, we can write unit tests easily.
3b. Service Testing
Like in the handler testing section, we need to mock our repository to test our service layer:
mockgen -source repository/movie_repository_interface.go -destination repository/mock_movie_repository.go -package repository
I will give you a trick. Whenever we change interfaces that we give to mockgen
, we have to run mockgen
command again.
Until now, we have two mock files and we need to write these long codes over and over when we add a new method to our interface. Such a tedious way! The solution is Makefile
. With the Makefile
, we use just generate-mocks
command to recreate our mock files.(make generate-mocks
)
We are testing possible errors by mocking the repository.
4. Repository
This is where our data integration is implemented:
Bonus: HTTP Client Plugin
We’re trying to develop RESTful API, and in the developing process, we want to sure it works as expected. To be sure, we need to send HTTP requests to our API.
You can do that both way using curl, Postman, Insomnia, and Jetbrains HTTP Client plugin, etc. I really like to use Jetbrains HTTP Client plugin.
With the HTTP Client plugin, you can create, edit, and execute HTTP requests directly in the IntelliJ IDEA code editor.
Source Code
References
[1,2] Clean Architecture A Craftsman’s Guide to Software Structure and Design (Robert C. Martin)
[3] 97 things every programmer should know collective wisdom from the experts by Kevlin Henney