Last time we built a strong foundation for our Blog based on the concepts of Clean Architecture. We set up rules and data structures of Article and Comment: primary building blocks of our app. In this post, we will take another step further: we are going to write Services.
What is a “Service”? Depending on the context and who you are talking with, you may receive completely different definitions and explanations. In our case, I will boldly name them as “something that performs core business operations.” From an architectural point of view, it’s the next circle in the dependency diagram.
From a technical perspective, it can be anything: class, function, a cluster of functions, and an object with methods. The only rule: keep it away from “details”: frameworks, stores, UI, etc. The only dependency Service should have are Entities or other Services.
From a conceptual point of view, the goal of services is to perform required business operations on Entities. If you are familiar with Uncle’s Bob book “Clean Architecture,” you may notice some deviation. In his book, Bob Martin described the next circle as “use cases.” I am not reinventing the wheel but combine use cases under the umbrella of “service(s).” If you are familiar with the Repository-Service pattern, which is quite popular in the backend world, you may find a lot of similarities.
Let’s start our journey from the analysis of requirements. The application is a Blog, but we won’t replicate all the possible functionality. Let’s assume that our Business Team and we use an incremental approach, and for now, we are building Minimal Valuable Product: MVP.
BT came to us with the following stories:
- As a User, I should be able to see all the Articles on the Home page
- As a User, I should be able to navigate from there to a page that represents one particular Article and see full Article
- As a User, I should be able to leave a comment on this page
(just a reminder, you can find a working DEMO example here)
These are quite long stories, and for now, we are not concerned about the UI part of them. We are thinking, “data first.”. We can transform them into technical tasks that aim only data/logic:
- Implement a way to get all Articles
- Implement a way to get one Article by Id
- Implement a way to create a new Comment for Article
Source code is available in the repo. Feel free to switch to “entities.” branch in the project if you skipped the previous post. If you completed the Entities section and everything works as intended, you are good to go.
Regarding the Services folder structure, I will follow the same approach we used for Entities:
contains all Articles Services code, specifically:/src/services/articles/articles.ts
actual Article Service/src/services/articles/articles.types.ts
types for Article Service/src/services/articles/articles.spec.ts
unit tests for Article Service/src/services/articles/articles.mocks.ts
mocks for Article Service/src/services/articles/index.ts
barrel file for Article Service/src/services/index.ts
barrel file for all Services
Let’s create these files. I also will fill in the barrel files:
Great, it’s time for us to write some types and tests. According to requirements, there are three use cases: get all Articles, get one Article by id and create a comment for Article. Let’s define signatures for them:
The first method is straightforward: grabs and returns all Articles.
getOneById expects a number ID as an argument and returns one Article if it exists or undefined otherwise. And finally, “createComment” expects Comment data and ID of Article to attach newly created Comment.
Note that we could create a separate Service for Comments. But since there is no dedicated API (or separate JSON file) for Comments, I didn’t find it necessary. But your real-life application might be different.
Now, since Service is a plain TS/JS code and we created mocks for Article and Comment Entities beforehand, it’s absolute ease and pleasure to write the tests:
The last two tests deserve special attention. If data given to create a comment is not valid, I expect an error to be thrown. Service should check in with the Entity and validate the provided data, otherwise scream with the error. I find this important to stress: Error Handling is often a crucial part of the application. Validation rules may not be defined nor controlled by UI: React/Vue components, Redux/Vuex Store, Thunks/Sagas, etc. Validation rules may only be managed by Entities, while Services utilize and enforce them.
Service, at your service!
It is time to write down some production code. Task feels clear: create a class and three methods. “getAll” for example, returns all data:
But what is data? If there were a remote backend/API, the “data” would be the result of a request to this API. But, if you recall, we use a local file to simplify things. So, in our case, data is the contents of data.json. We could import the file into Article.ts to access the data. Assuming that webpack is set up to import JSON files, there should be no technical issues with that. But there would be an architectural issue.
You see, importing this file means that service structurally depends on it. What if the source changes: instead of “data.json,” we now have “articles.json”? This change has nothing to do with Service per se. That means, we violate the Single Responsibility Principle:
“Gather together the things that change for the same reasons. Separate those things that change for different reasons”.
Plus, imagine there are a few services that consume the same data. You would probably want to load the file once and pass it to all services rather than keep importing again and again. So, keeping that in mind, let’s take one step farther in our architectural journey and “inject” data into the Service:
Now, whoever instantiates the service, must provide an instance of data. In more complicated cases, we could inject Axios, RxJS, or any other low-level tool that helps us get and operate data.
Note: I want to stress the term low-level. You should not inject heavy frameworks or libraries that tightly coupled with UI: like Redux-Saga or Thunk. Doing so, you couple Services with the UI/Store, and this is one of the things we try to avoid. Once again: keep Services as pure as possible.
Great, all pieces are here. Let’s implement the remaining methods:
And now, it’s time to clean up the tests. I create an instance of ArticleService and provide a mock to a constructor:
I also create an ArticleService mock:
Our Service looks great. But there is one single bit that is missing. Imagine, we have a few places in code that use courtesy of ArticleService. To do that, they’d have to:
- load data.json
- create an instance of ArticleService and provide data
It is not a clear way. Loading data and instantiating service multiple times can be easily avoided if there is a Service Provider. It is quite a common pattern that takes the trouble of instantiating multiple services and supporting them with necessary data. These instances can then be injected into “consumers”: actions, UI components, other Services, etc.
I will place Provider in the root of Services folder since it operates on services:
Let’s define a type for the provider.
Our application contains only one Service, so Provider holds a reference to only ArticleService. When the real-life application grows, Provider has more and more Services under its wing.
The test is reasonably easy to accomplish: we should make sure that ArticleService is instantiated:
The provider itself is simply a function that instantiates and returns a reference to all services:
And finally, let’s define a mock and update types and barrel file:
At this point, our code should compile without errors, and all tests should pass with 100% coverage. Completed tutorial can be found under “services” branch in the repo
In this chapter, we learned how to manipulate entities and data to fulfill use cases presented to us by Business Team. We tested our Services to ensure they do what they are supposed to. We also built “a bridge” between services and any other piece of the code that is going to use them (store, UI components) by defining Service Provider. We will use the Provider in the next chapters.
The beauty is that Entities and Services are completely independent of UI solutions. You may choose React or Vue, with Redux or without, with Vuex or without, and the application will work seamlessly since core business functionality is managed by pure code within Services and Entities.
Next time we are going to build the Store and see how it and Services can play together.
This is a second chapter in the series of posts “Building Vue Enterprise Application”. Other episodes are available here: