Infrastructure Layer Implementation Example based on @azure-net/kit
Preparation Stage
It is assumed that all types were described in the domain layer before the repository creation stage.
1. Response
We define the backend response structure and implement Response based on it. Using data sources without base responses is not recommended since the API can change at any time or the base backend response can be extended with new data (meta, pagination, etc.). The base response class will allow us to add keys for global common typing across the entire project at any time, as well as adjust all responses to a convenient structure. Therefore, it is highly recommended to initially create a base response, and if needed, it is possible to create several response classes for different situations or different APIs and integrations.
The base response is recommended to be created in /src/app/core/responses (can also be generated via CLI; it also uses this folder, or the responses folder in the infrastructure of the selected context).
If we know that there are several global contexts and different APIs with different responses, then we create one for each in each context in /src/app/contexts/{ContextName}/infrastructure/http/responses
import { ResponseBuilder } from '@azure-net/kit/infra';
export interface IBackendApiDataSourceResponse<T = unknown> {
data: T;
success: boolean;
message: string;
}
export class AzureNetApiResponse<TData = unknown, TMeta = unknown> extends ResponseBuilder<TData, TMeta, IBackendApiDataSourceResponse<TData>> {
// Unwrap data to avoid constantly dragging `data.data`
override unwrapData(data: IBackendApiDataSourceResponse<TData>): TData {
return data.data;
}
// Let's imagine a situation with a Yii2 backend, which out of the box returns pagination in headers (keys in meta are auto-typed when calling the method)
paginate() {
return this.addMeta({ page: Number(this.response.headers.page), total: Number(this.response.headers.total) });
}
}2. Datasource
We need a data source to use the httpService passed to it with a base URL, a queryBuilder that will help us process queries, and to define a method where we'll use our response. There are no restrictions on the method name, but it must use createRawRequest.
The base data source is recommended to be created in /src/app/core/datasources (can also be generated via CLI; it also uses this folder, or the datasources folder in the infrastructure of the selected context).
Like with responses, we can create different data sources in different contexts (/src/app/contexts/{ContextName}/infrastructure/http/datasources).
import { BaseHttpDatasource, type CreateRequestCallbackType } from '@azure-net/kit/infra';
import { AzureNetApiResponse, type IBackendApiDataSourceResponse } from '../responses/AzureNetApiResponse';
export class AzureNetHttpDatasource extends BaseHttpDatasource {
async createRequest<T>(callback: CreateRequestCallbackType<IBackendApiDataSourceResponse<T>>) {
return new AzureNetApiResponse<T, unknown>(await this.createRawRequest<IBackendApiDataSourceResponse<T>>(callback));
}
}3. Provider for datasource
Now we have a ready base for making requests, and we should create a provider to provide access to the data source to other providers.
A provider for the data source, if there is one, is recommended to be created in /src/app/core/providers. In case of different data sources for different contexts, create them in contexts (/src/app/contexts/{ContextName}/infrastructure/providers).
import { createBoundaryProvider, UniversalCookie } from '@azure-net/kit';
import { HttpService } from '@azure-net/kit/infra';
// Alias and export generated using CLI
import { AzureNetRestDatasource } from '$core';
export const DatasourceProvider = createBoundaryProvider('CoreDatasourceProvider', {
register: () => ({
AzureNetRestDatasource: () =>
new AzureNetRestDatasource({
http: new HttpService({
baseUrl: `https://api.example.ru`,
onRequest: (options) => {
// Send authorization header if token in cookie exists with each request
const token = UniversalCookie.get('token');
if (token) {
options.headers = { ...options.headers, Authorization: `Bearer ${token}` };
}
}
})
// If we don't pass QueryBuilder, the standard one with standard settings will be used
})
})
});4. DTO (Optional)
If we have a case where our domain model differs from what comes from the backend, and we need it in our application in our own way, then we can create a DTO and bring everything to the format we need. The number of DTOs is not limited; it is recommended to create folders with aggregate names inside the dto folder just in case (/src/app/contexts/{ContextName}/infrastructure/http/dto).
// These paths and aliases are generated via CLI; you're free to choose if not using CLI
import type {IUser, IUserFromBack} from '${ContextName}/domain/user'; // IUserFromBack ports folder, IUser model folder
class UserDTO extends DTOMapper<IUser> {
id: number;
name: string;
last_name: string;
// Let's imagine a situation where we display only full_name everywhere and don't want to constantly write the same thing in code, and the backend sends first_name and last_name
full_name: string;
constructor(data: IUserFromBack) {
super();
this.id = data.id;
this.name = data.first_name;
this.last_name = data.last_name;
this.full_name = this.getFullName();
}
getFullName() {
return `${this.name} ${this.last_name}`;
}
}5. Repository
Create a repository with the methods we need.
import { AzureNetRestDatasource } from '$core';
import { UserDTO } from '../dto/user';
import type { ILoginRequest, ILoginResponse } from '${ContextName}/domain/auth'; // ports folder
import type { IUserFromBack } from '${ContextName}/domain/user'; // ports folder
export class AuthRepository {
constructor(private azureNetRestDatasource: AzureNetRestDatasource) {}
public async login(request: ILoginRequest) {
return this.azureNetRestDatasource
.createRequest<ILoginResponse>(({ http }) => http.post('/auth/login', { json: request }))
.then((res) => res.getData());
}
// What the method returns is auto-typed as Promise<IUser>
public async current() {
return this.azureNetRestDatasource.createRequest<IUserFromBack>(({ http }) => http.get('/auth/current')).then((res) => res.mapUsing(UserDTO).getData());
}
public async logout() {
return this.azureNetRestDatasource.createRequest<void>(({ http }) => http.post('/auth/logout')).then((res) => res.getData());
}
}6. Provider for infrastructure
As the final stage, we create a provider for infrastructure, inject DatasourceProvider into it, and register the repository to provide it to the application layer. After this stage, we can work in the application layer with this repository, as soon as we inject our InfrastructureProvider into ApplicationProvider.
import { createBoundaryProvider } from '@azure-net/kit';
import { DatasourceProvider } from '$core';
import { AuthRepository } from '../http/repositories';
export const InfrastructureProvider = createBoundaryProvider('{ContextName}InfrastructureProvider', {
dependsOn: { DatasourceProvider },
register: ({ DatasourceProvider }) => ({
AuthRepository: () => new AuthRepository(DatasourceProvider.AzureNetRestDatasource),
// Other repositories...
})
});