ExpressoTS as you may already know, is a lightweight but very robust backend typescript framework. As part of his continuous growth, the developer community has requested more flexibility in the way Middleware and Controller can be used in the application.
Guess what? We are delivering in the v2.8.0, the state of the art for handling, creating and configuring middleware and controllers with ExpressoTS.
Middleware
Middleware is a function that has access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.
ExpressoTS fully supports Express middleware.
Adding Middleware
ExpressoTS application supports adding middleware globally to the application as well as per route. It offers all the middleware supported by Express Team out-of-the-box through the use of this.middleware
property.
@provide(App)
export class App extends AppExpress {
private middleware: IMiddleware;
constructor() {
super();
this.middleware = container.get<IMiddleware>(Middleware);
}
protected configureServices(): void {
this.middleware.addBodyParser();
this.middleware.addCors();
this.middleware.addHelmet();
}
}
If you add a middleware that is not installed as dependency, the application will throw a warning message and continue to run.
Global Middleware
Middlewares can be added globally using the App
class through the this.middleware
property, using the out-of-the-box middleware list provided by Express Team:
protected configureServices(): void {
this.middleware.addBodyParser();
this.middleware.addCors();
this.middleware.addHelmet();
}
For any other middleware, or a custom middleware, you can add it using the this.middleware.addMiddleware()
method. Using the addMiddleware
method, you can add any middleware from NPM Registry, custom Expressjs middleware or a custom ExpressoTS middleware.
NPM Registry middleware:
this.middleware.addMiddleware(cors());
Custom Expressjs middleware:
function myMiddleware(req: Request, res: Response, next: NextFunction) {
// Do something
next();
}
this.middleware.addMiddleware(myMiddleware);
Custom ExpressoTS middleware:
class CustomMiddleware extends ExpressoMiddleware {
use(req: Request, res: Response, next: NextFunction): void | Promise<void> {
// Do something
next();
}
}
this.middleware.addMiddleware(new CustomMiddleware());
Route Middleware
Middlewares can be added per route in the App
class through the this.middleware.addMiddleware()
method. You can add any middleware from NPM Registry, custom Expressjs middleware or a custom ExpressoTS middleware.
this.middleware.addMiddleware({ path: "/api", middlewares: [] });
Each route can have multiple middlewares. Or you add a middleware to a specific route in the Controller
class through the @controller()
and/Or http Method
decorators.
@controller("/")
export class AppController {
@Post("", express.json())
execute() {
return "Hello World";
}
}
If you want to apply a middleware to all routes under a specific controller, you can add it to the @controller()
decorator.
@controller("/app", express.json())
export class AppController {
@Post("/create")
createApp() {
return "Create App";
}
@Patch("/update")
updateApp() {
return "Update App";
}
}
Creating Custom ExpressoTS Middleware
To create a custom middleware, you need to extend the ExpressoMiddleware
class and implement the use
method.
class CustomMiddleware extends ExpressoMiddleware {
private isOn: boolean;
constructor(isOn: boolean) {
super();
this.isOn = isOn;
}
use(req: Request, res: Response, next: NextFunction): void | Promise<void> {
// Do something
if (this.isOn) {
next();
} else {
res.status(403).send("Forbidden");
}
}
}
Custom middleware allows you to pass parameters to the constructor and use them as options in the use
method of your middleware.
Use ExpressoTS CLI to scaffold a custom middleware. CLI command to scaffold a custom middleware:
expressots g m <<middleware-name>>
Controllers
Controllers act as the primary interface between the client and server in ExpressoTS applications. They handle incoming requests, validate payloads against an input DTO, and emit responses in the DTO pattern. In essence, controllers bridge the communication between clients and service layers, also known as use-cases.
Controller MVC Pattern Example
Controller Single Responsibility Pattern Example
In order to create a basic controller we create a class and use the @controller()
decorator to define the route for the controller. Then we create a method and use the http Methods
decorators to define the route and HTTP method for the method.
Controller Class
MVC Pattern
Multiple routes in a single controller.
@controller("/user")
export class UserController {
@Post("/")
create(@response() res: Response) {
return res.send("User created");
}
@Get("/")
getUser(@response() res: Response) {
return res.send("User listed");
}
}
Single Responsibility Pattern
One controller, one execution method, and one use case.
@controller("/user/create")
export class UserCreateController {
@Post("/")
execute(@response() res: Response) {
return res.send("User created");
}
}
Both, the @controller()
and http methods
decorators works in combination to define the route and middlewares to be used in the endpoint.
Controller Scope
The default scope of a controller is Request
, as it is inherited from the AppContainer
and default Module
class scope. However, you can override the scope of a controller by using the @scope()
decorator. This decorator accepts the same BindingScopeEnum enum values.
If you define the module scope you can not override it in a specific controller by using the @scope
decorator.
The @scope
decorator can only be used in specific controllers if the module scope is not defined.
Here is an example of usage:
@scope(BindingScopeEnum.Singleton)
@controller("/")
class CreateUserController extends BaseController {}
The controller above will be scoped as Singleton
and will be shared across all requests.
BaseController Class
We also power a controller class with the BaseController
class that offers in the constructor a parameter that the developer can indicate what service or domain he is currently building. This helps the log system throw errors with more information about the context of the error and where the error occurred.
Another advantage of using the BaseController
class is that it offers a method in two different versions async and sync, which is the callUseCase() or callUseCaseAsync()
.
These methods reinforce the idea of one use case per controller, and they are responsible for calling the use case that will implement the business logic of the controller.
The callUseCase methods, available in both synchronous and asynchronous versions, are well-suited for returning status and JSON responses to clients, as the users just need to pass the useCase return, res:Response, and statusCode.
The signature of the callUseCase
method:
callUseCase(useCase: any, res: any, successStatusCode: number);
-
useCase
: The use case that will be called. This use case is injected in the controller constructor. -
res
: The response object from the express request. -
successStatusCode
: The status code that will be sent to the client if the use case is executed successfully. Please see the Http Status Code type for more information.
The default response:
return res.status(successStatusCode).json(dataReturn);
A more complete example of a controller class inheriting from the BaseController class is shown below:
@controller("/")
class AppController extends BaseController {
constructor(private appUseCase: AppUseCase) {
super("app-controller");
}
@httpGet("/")
execute(@response() res: any): IAppResponseDTO {
return this.callUseCase(this.appUseCase.execute(), res, StatusCode.OK);
}
}
Controller Validating DTO
Read the follow section for DTO Validation :: https://doc.expresso-ts.com/docs/providers/dto-validator
Top comments (0)