Implementing the Adapters Layer
Clean Architecture Series - Adapters Layer
The adapters layer is responsible for interacting with the outside world. It acts as a bridge between the business logic (use cases) and the external systems. For example, In a web application, It takes the incoming requests and transforms them into a format that the use cases can understand.
CreatePolicy Controller
To handle our create policy feature, we need to create a controller to mapping the web server routes of the CreatePolicy endpoint which we will define in the framework layer with ExpressJS.
So, go to the "adapters" folder and create a new folder named "controllers/policy" with a "create-policy.controller.ts" file.
import { IHttpRequest } from "@/adapters/http/http-request.interface";
import { HttpResponse } from "@/adapters/http/http-response.type";
import { IPolicyRepository } from "@/domain/policy/policy-repository.interface";
import { CreatePolicyUseCase } from "@/use-cases/policy/create-policy.use-case";
export class CreatePolicyController {
constructor(private policyRepository: IPolicyRepository) {}
async execute(httpRequest: IHttpRequest): Promise<HttpResponse> {
try {
const createPolicyUseCase = new CreatePolicyUseCase(this.policyRepository);
const policy = await createPolicyUseCase.execute({ ...httpRequest.getBody() });
return { httpCode: 201, payload: policy };
} catch (error) {
return {
httpCode: 500,
error: {
name: "CreatePolicyController",
message: "Error",
details: error.message,
},
};
}
}
}
The class constructor
The constructor injection ensures that the controller cannot be instantiated without its necessary dependencies. In this case, we have the IPolicyRepository dependency which is required by controller to create the "CreatePolicyUseCase" instance.
The execute method
By accepting dependencies through a setter method, rather than the constructor, allows ExpressAdapter to manipulate each incoming request.
The "execute" method injects the use case dependencies, pass the body data and call the execute method of the use-case.
Finally, in case of success, it returns 201 with the new Policy entity object else an error object according to the HttpResponse type is returning.
Why IHttpRequest and IPolicyRepository?
These interfaces allows us to use the Inversion of Control (IoC) principle and Dependency Injection (DI) technique to delegate the responsibility of handling dependencies and object creation to the external layers.
We are using the IHttpRequest interface as parameter of the execute method to ensure that our controller is agnostic on any framework or library. And the IPolicyRepository interface as a dependency in the constructor to leave open the option to switch between repository implementations.
\ We can switch from one specific implementation to another easily (Ex. replace Expressjs with Fastifyjs or replace Sqlite3 with another DB).*
Let's create the HttpRequest Interface file named "http-request.interface.ts"
//src/adapters/http/http-request.interface.ts
export interface IHttpRequest {
getBody: () => any;
}
We only define one interface member named getBody to handle the body data from the "ExpressRequest" Function (we will use it in the Express Adapter section).
\You can define more members of the interface if you wanted to have more control over the Request. For example: getHeaders to handle the Request's Headers.*
Express Adapter
As we chose Expressjs as web server, a package of our frameworks layer, we need to create an adapter to adapt the ExpressJS's request-response cycle.
So, go to the "adapters" folder and create a new folder named "http" with a "express.adapter.ts" file.
import {
Request as ExpressRequest,
Response as ExpressResponse,
NextFunction as ExpressNextFunction,
} from "express";
import { IHttpRequest } from "./http-request.interface";
import { HttpResponse } from "./http-response.type";
export class ExpressAdapter {
static controller(fn: (...args: any) => Promise<HttpResponse>) {
return (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {
fn(ExpressAdapter.adaptRequest(req))
.then((httpResponse: HttpResponse) => {
const { httpCode, ...rest } = httpResponse;
res.status(httpCode).json(rest);
})
.catch((error: Error) => {
res.status(500).send(error.message);
});
};
}
static adaptRequest(req: ExpressRequest): IHttpRequest {
return {
getBody: () => req.body,
};
}
}
The static controller method
The "controller" method create and return a middleware function to handle the "Expressjs" request-response" cycle. It has access to "Expressjs" objects and allows to use the ExpressRequest, ExpressResponse and ExpressNextFunction:
...
static controller(fn: (...args: any) => Promise<HttpResponse>) {
...
return (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => {}
Then, calls the provided function from the "fn" parameter and adds the adapted request object according the IHttpRequest interface:
fn(ExpressAdapter.adaptRequest(req))
.then((httpResponse: HttpResponse) => {
const { httpCode, ...rest } = httpResponse;
res.status(httpCode).json(rest);
})
.catch((error: Error) => {
res.status(500).send(error.message);
});
\The "fn" parameter represents the controller method.*
As the "fn" returns a Promise, this method registers "then" and "catch" functions:
Then: When "then" function is calling, the adapter takes the controller's ("fn") response and use the ExpressJs response object to generate the http response with the controller response data as a json object.
Catch: When "catch" is calling, the adapter takes the controller's ("fn") error and use the ExpressJs response object to generate the http response with the controller response data.
\We only use the 500 error for everything to simplify the error handling. You could improve it having custom exceptions as you need.*
The static adaptRequest method
Last but not least, the HttpResponse type file named "http-response.type.ts":
// ./src/adapters/http/http-response.type.ts
export type HttpResponse = {
httpCode: number;
error?: {
name: string;
message: string;
details: any;
};
payload?: any;
};
This type allows us to define a common response object for the API response.
Now the adapter layer is ready to be integrated in our Clean Architecture project.
See you in the next article.