Middleware

Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects, and the next() middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.

Middleware - 图1

Nest middleware are, by default, equivalent to express middleware. The following description from the official express documentation describes the capabilities of middleware:

Middleware functions can perform the following tasks:
  • execute any code.
  • make changes to the request and the response objects.
  • end the request-response cycle.
  • call the next middleware function in the stack.
  • if the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging.

You implement custom Nest middleware in either a function, or in a class with an @Injectable() decorator. The class should implement the NestMiddleware interface, while the function does not have any special requirements. Let’s start by implementing a simple middleware feature using the class method.

  1. @@filename(logger.middleware)
  2. import { Injectable, NestMiddleware } from '@nestjs/common';
  3. import { Request, Response } from 'express';
  4. @Injectable()
  5. export class LoggerMiddleware implements NestMiddleware {
  6. use(req: Request, res: Response, next: Function) {
  7. console.log('Request...');
  8. next();
  9. }
  10. }
  11. @@switch
  12. import { Injectable } from '@nestjs/common';
  13. @Injectable()
  14. export class LoggerMiddleware {
  15. use(req, res, next) {
  16. console.log('Request...');
  17. next();
  18. }
  19. }

Dependency injection

Nest middleware fully supports Dependency Injection. Just as with providers and controllers, they are able to inject dependencies that are available within the same module. As usual, this is done through the constructor.

Applying middleware

There is no place for middleware in the @Module() decorator. Instead, we set them up using the configure() method of the module class. Modules that include middleware have to implement the NestModule interface. Let’s set up the LoggerMiddleware at the AppModule level.

  1. @@filename(app.module)
  2. import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
  3. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  4. import { CatsModule } from './cats/cats.module';
  5. @Module({
  6. imports: [CatsModule],
  7. })
  8. export class AppModule implements NestModule {
  9. configure(consumer: MiddlewareConsumer) {
  10. consumer
  11. .apply(LoggerMiddleware)
  12. .forRoutes('cats');
  13. }
  14. }
  15. @@switch
  16. import { Module } from '@nestjs/common';
  17. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  18. import { CatsModule } from './cats/cats.module';
  19. @Module({
  20. imports: [CatsModule],
  21. })
  22. export class AppModule {
  23. configure(consumer) {
  24. consumer
  25. .apply(LoggerMiddleware)
  26. .forRoutes('cats');
  27. }
  28. }

In the above example we have set up the LoggerMiddleware for the /cats route handlers that were previously defined inside the CatsController. We may also further restrict a middleware to a particular request method by passing an object containing the route path and request method to the forRoutes() method when configuring the middleware. In the example below, notice that we import the RequestMethod enum to reference the desired request method type.

  1. @@filename(app.module)
  2. import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
  3. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  4. import { CatsModule } from './cats/cats.module';
  5. @Module({
  6. imports: [CatsModule],
  7. })
  8. export class AppModule implements NestModule {
  9. configure(consumer: MiddlewareConsumer) {
  10. consumer
  11. .apply(LoggerMiddleware)
  12. .forRoutes({ path: 'cats', method: RequestMethod.GET });
  13. }
  14. }
  15. @@switch
  16. import { Module, RequestMethod } from '@nestjs/common';
  17. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  18. import { CatsModule } from './cats/cats.module';
  19. @Module({
  20. imports: [CatsModule],
  21. })
  22. export class AppModule {
  23. configure(consumer) {
  24. consumer
  25. .apply(LoggerMiddleware)
  26. .forRoutes({ path: 'cats', method: RequestMethod.GET });
  27. }
  28. }

info Hint The configure() method can be made asynchronous using async/await (e.g., you can await completion of an asynchronous operation inside the configure() method body).

Route wildcards

Pattern based routes are supported as well. For instance, the asterisk is used as a wildcard, and will match any combination of characters:

  1. forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

The 'ab*cd' route path will match abcd, ab_cd, abecd, and so on. The characters ?, +, *, and () may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( -) and the dot (.) are interpreted literally by string-based paths.

Middleware consumer

The MiddlewareConsumer is a helper class. It provides several built-in methods to manage middleware. All of them can be simply chained in the fluent style. The forRoutes() method can take a single string, multiple strings, a RouteInfo object, a controller class and even multiple controller classes. In most cases you’ll probably just pass a list of controllers separated by commas. Below is an example with a single controller:

  1. @@filename(app.module)
  2. import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
  3. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  4. import { CatsModule } from './cats/cats.module';
  5. import { CatsController } from './cats/cats.controller.ts';
  6. @Module({
  7. imports: [CatsModule],
  8. })
  9. export class AppModule implements NestModule {
  10. configure(consumer: MiddlewareConsumer) {
  11. consumer
  12. .apply(LoggerMiddleware)
  13. .forRoutes(CatsController);
  14. }
  15. }
  16. @@switch
  17. import { Module } from '@nestjs/common';
  18. import { LoggerMiddleware } from './common/middleware/logger.middleware';
  19. import { CatsModule } from './cats/cats.module';
  20. import { CatsController } from './cats/cats.controller.ts';
  21. @Module({
  22. imports: [CatsModule],
  23. })
  24. export class AppModule {
  25. configure(consumer) {
  26. consumer
  27. .apply(LoggerMiddleware)
  28. .forRoutes(CatsController);
  29. }
  30. }

info Hint The apply() method may either take a single middleware, or multiple arguments to specify multiple middlewares.

Quite often we might want to exclude certain routes from having the middleware applied. When defining middleware with a class (as we have been doing so far, as opposed to using the alternative functional middleware), we can easily exclude certain routes with the exclude() method. This method takes one or more objects identifying the path and method to be excluded, as shown below:

  1. consumer
  2. .apply(LoggerMiddleware)
  3. .exclude(
  4. { path: 'cats', method: RequestMethod.GET },
  5. { path: 'cats', method: RequestMethod.POST }
  6. )
  7. .forRoutes(CatsController);

With the example above, LoggerMiddleware will be bound to all routes defined inside CatsController except the two passed to the exclude() method. Please note that the exclude() method does not work with functional middleware (middleware defined in a function rather than in a class; see below for more details). In addition, this method doesn’t exclude paths from more generic routes (e.g., wildcards). If you need that level of control, you should put your paths-restriction logic directly into the middleware and, for example, access the request’s URL to conditionally apply the middleware logic.

Functional middleware

The LoggerMiddleware class we’ve been using is quite simple. It has no members, no additional methods, and no dependencies. Why can’t we just define it in a simple function instead of a class? In fact, we can. This type of middleware is called functional middleware. Let’s transform the logger middleware from class-based into functional middleware to illustrate the difference:

  1. @@filename(logger.middleware)
  2. export function logger(req, res, next) {
  3. console.log(`Request...`);
  4. next();
  5. };

And use it within the AppModule:

  1. @@filename(app.module)
  2. consumer
  3. .apply(logger)
  4. .forRoutes(CatsController);

info Hint Consider using the simpler functional middleware alternative any time your middleware doesn’t need any dependencies.

Multiple middleware

As mentioned above, in order to bind multiple middleware that are executed sequentially, simply provide a comma separated list inside the apply() method:

  1. consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

Global middleware

If we want to bind middleware to every registered route at once, we can use the use() method that is supplied by the INestApplication instance:

  1. const app = await NestFactory.create(AppModule);
  2. app.use(logger);
  3. await app.listen(3000);