In almost all applications there is a file that throws all the application's endpoints. If you're using Angular, it might look like this:
export const API = new InjectionToken('API');
const API_URL = `${environment.BASE_URL}/api/v1`;
export const endpoints = {
login: '${API_URL}/auth/login',
signup: '${API_URL}/auth/signup',
createTodo: '${API_URL}/todos/create',
updateTodo: '${API_URL}/todos/:id/update',
deleteTodo: '${API_URL}/todos/:id/delete,
.....
};
At first glance, everything looks fine, but this file creates some problems as the application grows.
We violated the Single Responsibility Principle. We're facing merge conflicts, our endpoints are not portable, and it's hard to find the endpoint location in the file.
We need to assign responsibility to the responsible modules, so each module is responsible for exposing its endpoints.
Then we can aggregate and join the entire endpoint in the API service.
Let's see how to achieve this with the help of Angular dependency injection and the multi option.
Create API service
First, we need to create the service responsible for performing the aggregation and exposing the application endpoints.
import {Inject, Injectable, InjectionToken} from '@angular/core';
export const END_POINTS = new InjectionToken('END_POINTS');
@Injectable()
export class API {
private readonly _baseUrl = environment.BASE_URL;
endPoints: EndPoints;
constructor(@Inject(END_POINTS) private _endPoints) { }
}
api.service.ts
We have one representing each endpoint InjectionToken
. Now let's see how we populate the END_POINTS
token.
Create an authentication module
Two additional files need to be created for each module.
export const api = {
login: "/auth/login",
logout: "/auth/logout",
signup: "/auth/signup",
};
auth.api.ts
As the name suggests, this file will be responsible for the Auth endpoint.
interface EndPoints {
login: string;
logout: string;
signup: string;
}
auth.d.ts
We still want to use the advantages of typescript declaration merging .
Fundamentally, the mechanism of merging is to put the members of both parties into an interface of the same name.
Now, typescript will merge each endpoint into an EndPoints
interface so we can autocomplete.
Next, we provide the Auth
endpoint to Angular dependency injection in the appropriate module, in our case the Auth
module.
import { END_POINTS } from "../config/api";
import { api } from "./auth.api";
const AUTH_API = { provide: END_POINTS, multi: true, useValue: api };
@NgModule({
...
providers: [AuthService, AUTH_API]
})
export class AuthModule {
}
auth.module.ts
You can think of multi
options as an array. Every time we add a new one provider
, Angular provider
will push into the array.
So if we go back to our API service and print the _endpoints
properties, we will see that there is one item in the array - the Auth
endpoint.
END_POINTS Provider
Great, now we just need to flatten our endpoints onto a large object.
@Injectable()
export class API {
...
constructor(@Inject(END_POINTS) private _endPoints) {
this.endPoints = _endPoints.reduce((acc, current) => {
return {
…current,
…acc
};
}, {});
}
}
api.service.ts
Finally, we create a simple method to help us parse the url.
class API {
...
resolve(url: string, params?) {
if (!params) {
return `${this._baseUrl}${url}`;
}
const resolved = Object.keys(params).reduce((acc, param) => {
return acc.replace(`:${param}`, params[param]);
}, url);
return `${this._baseUrl}${resolved}`;
}
}
api.service.ts (Translator's Note: This function is used to replace :id
the real parameters in the url)
Now we can use the API service in every service we need. For brevity, I've added a Todos
module where we describe the same process. example:
Summarize
In this article, we witnessed the power of Angular Dependency Injection and how we can leverage the multi
options. With this change, we are less susceptible to merge conflicts, and the modules are portable with the API, so we can use them with other applications. We also avoid violating the Single Responsibility Principle, and our code is better organized.