DEV Community

Cover image for ExpressoTS Middleware & Controller
Richard Zampieri for ExpressoTS

Posted on

ExpressoTS Middleware & Controller

ExpressoTS Doc

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

Custom Expressjs middleware:

function myMiddleware(req: Request, res: Response, next: NextFunction) {
    // Do something
    next();
}
Enter fullscreen mode Exit fullscreen mode
this.middleware.addMiddleware(myMiddleware);
Enter fullscreen mode Exit fullscreen mode

Custom ExpressoTS middleware:

class CustomMiddleware extends ExpressoMiddleware {
    use(req: Request, res: Response, next: NextFunction): void | Promise<void> {
        // Do something
        next();
    }
}
Enter fullscreen mode Exit fullscreen mode
this.middleware.addMiddleware(new CustomMiddleware());
Enter fullscreen mode Exit fullscreen mode

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: [] });
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>>
Enter fullscreen mode Exit fullscreen mode

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 MVC Pattern

Controller Single Responsibility Pattern Example

Controller Single Responsibility Pattern

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Controller Validating DTO

Read the follow section for DTO Validation :: https://doc.expresso-ts.com/docs/providers/dto-validator

Top comments (0)