Nest.js

[Nest.js] Life Cycle - 1 Middleware

ESTJames 2021. 10. 1. 14:48

 

1. What is Middleware?

Nest.js documentation


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.Nest middleware are equivalent to Express middleware.Nest Middleware fully supports Dependency Injection.

 

A middleware class implements NestMiddleware interface, and its abstract use method.

We can access represent Request, Response objects very easily.

Also, it has next function to finish this middleware and send a request steam to the next step, such as another middleware or guards.

If we don’t call next method, it never move forward.

 

2. What Middleware does ?

1. execute any code
2. make changes to the request and the response objects
3. end the request-response cycle
4. call the next middleware function in the stack
5. 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.

OMG, it execute any code? seriously? then, why do we need other components ?

I feel like we only need middleware and controller to handle request-response ! Ha!

 

3. Why is Nest.js Middleware different from other components?

Middleware is called only before the route handler is called. 
We have access to the response object, 
but we don't have the result of the route handler.

I think it is used for Security things!!!

Registration
In the module, very flexible way of choosing relevant routes (with wildcards, by method,...)
In main.ts, Globally with app.use()

Examples
middleware that is out there. (link : http://expressjs.com/en/resources/middleware.html )
We can use npm to install Third-party middleware
There are lots of libraries, 
e.g. cors, body-parser, cookie-parser ,morgan, helmet

Conclusion

The registration of middleware is very flexible, 
for example: apply to all routes but one etc. 
But since they are registered in the module, 
we might not realize it applies to our controller when we're looking at its methods. 
It's also great that we can make use of all the express middleware libraries that are out there.

Middleware Implementation

1. Middleware type

  1.1 Class-based middleware

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction } from 'express';

@Injectable() // 1
export class LoggerMiddleware implements NestMiddleware { // 2
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Middleware working here'); // 3
    console.log('req', req); // 3
    console.log('res', res); // 3
    next(); // 4
  }
}

// 1. @Injectable() - it is injected dependency by constructor
// 2. implements NestMiddleware - class middleware have to implement "NestMiddleware" interface

export interface NestMiddleware<TRequest = any, TResponse = any> {
    use(req: TRequest, res: TResponse, next: () => void): any;
}


// 3. will check results later
// 4. next() - if we don't use next(), we CANNOT exit from this middleware and the request is left hanging.

 

  1.2 Functional middleware

the above middleware 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?  
Let's transform the logger middleware from 1.1class-based into 1.2functional middleware
import { Request, Response, NextFunction } from 'express';

export function functionalLogger(req: Request, res: Response, next: NextFunction) { // 1
  console.log('Functional Middleware working here');
  console.log('req', req); 
  console.log('res', res); 
  
  next();
 };

// 1. logger(req: Request, res: Response, next: NextFunction)
       - parameters are the same with the above #1.1 use(..) parameters.

 

2 Middleware Binding

  2.1 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

  2.1.1 Applying in main.ts

import { Logger, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as config from 'config';
import { hookLogger, winstonLogger } from './logger/winston.logger';

async function bootstrap() {
  const logger = new Logger();
  const app = await NestFactory.create(AppModule);

  const serverConfig = config.get('server');
  const port = serverConfig.port;

  app.use(logger);

  // Global Pipe
  app.useGlobalPipes(
    ...
  );

  await app.listen(port);

  ...
}
bootstrap();

// 1. app.use(middleware) 

 

  2.2 Module middleware

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.
// app.module.ts

import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { BoardsModule } from './boards/boards.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMConfig } from './configs/typeorm.config';
import { AuthModule } from './auth/auth.module';
import { LoggerMiddleware } from './middleware/logger.middleware';

@Module({
  imports: [
    TypeOrmModule.forRoot(TypeORMConfig),
    BoardsModule,
    AuthModule
  ],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) { 
    consumer 
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'boards', method: RequestMethod.POST },
        { path: 'boards', method: RequestMethod.PUT },
        'boards/(.*)', 
      )
      .forRoutes('boards');
  }
}

 

2. 3 Middleware Interfaces

What the hecks are NestModule, MiddlewareConsumer, and so on...
for module middleware binding?
Let's go much deeper side for it  !!!

   2.3.1 NestModule Interface

import { MiddlewareConsumer } from '../middleware/middleware-consumer.interface';
export interface NestModule {
    configure(consumer: MiddlewareConsumer): any;
}

 2.3.2 MiddlewareConsumer

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.
import { Type } from '../type.interface';
import { MiddlewareConfigProxy } from './middleware-config-proxy.interface';

export interface MiddlewareConsumer {
    apply(...middleware: (Type<any> | Function)[]): MiddlewareConfigProxy;
}

  2.3.3 MiddlewareConfigProxy

The exclude method can exclude routes from the currently processed middleware.

The forRoutes() method can take 
a single string, multiple strings, a RouteInfo object, a controller class and even multiple controller classes.
In most cases we'll probably just pass a list of controllers separated by commas. 
If you pass a class, Nest would attach middleware to every path defined within this controller.
Also, the asterisk is used as a wildcard and will match any combination of charaters.
e.g., forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
import { Type } from '../type.interface';
import { RouteInfo } from './middleware-configuration.interface';
import { MiddlewareConsumer } from './middleware-consumer.interface';
export interface MiddlewareConfigProxy {
    exclude(...routes: (string | RouteInfo)[]): MiddlewareConfigProxy;
    forRoutes(...routes: (string | Type<any> | RouteInfo)[]): MiddlewareConsumer;
}

Example - Helmet, Global Middleware

 

1. Helmet

Helmet can help protect your app from some well-known web vulnerabilities by setting HTTP headers appropriately.
Generally, Helmet is just a collection of 15 smaller middleware functions
that set security-related HTTP headers.

Each middleware's name is listed below.
1. helmet.contentSecurityPolicy(options) - default
2. helmet.crossOriginEmbedderPolicy()
3. helmet.crossOriginOpenerPolicy()
4. helmet.crossOriginResourcePolicy()
5. helmet.expectCt(options) - default
6. helmet.referrerPolicy(options) -default
7. helmet.hsts(options) - defulat
8. helmet.noSniff() - default
9. helmet.originAgentCluster() 
10. helmet.dnsPrefetchControl(options) - default
11. helmet.ieNoOpen() - default
12. helmet.frameguard(options) - default
13. helmet.permittedCrossDomainPolicies(options) - default
14. helmet.hidePoweredBy() - default
15. helmet.xssFilter() - default

more details of the above middlewares - https://github.com/helmetjs/helmet#reference

 

2. Helmet binding

The top-level helmet() is a wrapper around 15 smaller middlewares, 11 of which are enabled by default.

// This...
app.use(helmet());

// ...is equivalent to this:
app.use(helmet.contentSecurityPolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard());
app.use(helmet.hidePoweredBy());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());

 

3. Implementation

- Response header before helmet used

// as we can see "X-Powered-By" in response header, it might provide the information of software what we use.
   Therefore, a hacker will use the information to attack the server.

 

- module installation

$ npm install helmet --save

 

- global binding of a helmet middleware to hide "X-Powered-By" inforamation, helmet.hidePoweredBy()

...
import * as helmet from 'helmet';

async function bootstrap() {
  const logger = new Logger();
  const app = await NestFactory.create(AppModule);

  const serverConfig = config.get('server');
  const port = serverConfig.port;

  app.use(helmet.hidePoweredBy()); // 1
  
  ...

  await app.listen(port);

  ...
}
bootstrap();

// 1. app.use(helmet.hidePoweredBy()) - setting up one of the middlewares in helmet to hide the information.

 

- Response header after helmet.hidePoweredBy() used

// SEEE!!!!??? There is no information, so our project is in safe !(Not really)

 

- global binding of entire helmet in main.ts

...
import * as helmet from 'helmet';

async function bootstrap() {
  const logger = new Logger();
  const app = await NestFactory.create(AppModule);

  const serverConfig = config.get('server');
  const port = serverConfig.port;

  app.use(helmet()); // 1
  
  ...

  await app.listen(port);

  ...
}
bootstrap();

// 1. app.use(helmet()) - setting up the 11 middlewares as defualt

 

- Response header after helmet() used

// Wow, There are some information related to security