Angular Services are often used to make HTTP requests to a backend. Next to the actual HTTP call you may also transform the data before sending them to the component. The repository pattern can help you to make these services better and easier to test. Let's see what this pattern is and how we can implement it.
Basic Service
A simple Angular application with a backend may have a service to read data from the backend via HTTP requests. In addition, the service may also have some business logic to transform data. The service now have two responsibilities: accessing the backend and applying business logic.
@Injectable({ providedIn: "root" })
export class ProductService {
constructor(private httpClient: HttpClient) {}
fetchProducts() {
return this.httpClient.get<Product[]>("/products").pipe(
map((products) => {
// Some transformation ...
return transformedProducts;
})
);
}
}
Now let us apply the repository pattern.
About the pattern
According to Martin Fowler, a repository:
... mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer.
The repository pattern separates the code responsible for accessing the data from the rest of the code. We can now use this pattern in our Angular application.
Apply the pattern
With the repository pattern we move the data fetching part into a repository.
The repository is responsible for accessing the backend using the HttpClient. It does not apply any business logic. The main service does not need to know anything about the backend or its (RESTful) API anymore.
@Injectable()
export class ProductService {
constructor(private productRepository: ProductRepository) {}
fetchProducts() {
return this.productRepository.fetchProducts().pipe(...);
}
}
Everything related to data fetching has been moved into a new repository class.
@Injectable()
export class ProductRepository {
constructor(private httpClient: HttpClient) {}
fetchProducts() {
return this.httpClient.get<Product[]>("/products");
}
}
Advantages
The repository pattern brings a few advantages.
1. Better separation of concerns
Your services are smaller and better maintainable.
They contain business logic and transform your data but don't care about how to fetch data.
2. Repositories can be reused
A repository can be used by multiple services. This allows
other services to use the raw data provided by the backend.
3. Easier testing of services
When you write unit and integration tests for your services (I'm sure you do) you don't want to actually make HTTP calls to a backend but instead mock them.
One approach is using the HttpClientTestingModule
. While this may be fine for testing the actual HTTP calls (like if parameters are provided properly), it's a bit awkward for mocking HTTP calls and more importantly doesn't work together with RxJS Marble Testing (not familiar with it? Give it a try, it's awesome).
With the repository you can simply mock the repository when testing your services. At that moment you don't care about the repository implementation, and you can simply mock its methods.
Final thoughts
We use this pattern in our team for some applications where we benefit from the advantages mentioned above. Adding additional classes, the repositories, also brings some overhead. Therefore, you should consider if it really makes sense for your use cases.
In the previous examples the backend URL was hard-coded directly in the service / repository. I recommend moving all API endpoints into a configuration object and providing it with an Injection Token. This makes it easier to maintain them and keeps your classes even cleaner.