Middleware Middleware

What is middleware used for?

  • Execute any code
  • Change the request/response object
  • 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, then next() must be called to pass control to the next middleware function. Otherwise, the request will remain pending.

Using middleware

Instead of implementing a middleware decorator, Nest uses the module class’s Configure method. Therefore, modules that require middleware need implements NestModule.

export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .forRoutes('cats'); }}Copy the code

Method of use

  1. The name of the routingforRoutes('cats')
  2. The controllerforRoutes(CatsController)
  3. Specific request typeforRoutes({ path: 'cats', method: RequestMethod.GET })
  4. Route wildcardforRoutes({ path: 'ab*cd', method: RequestMethod.ALL })
  5. Reject certain requests
consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);
Copy the code

Functional middleware

We can write middleware as a function:

import { Request, Response } from 'express'; export function logger(req: Request, res: Response, next: Function) { console.log(`Request... `); next(); };Copy the code

How to use functional middleware

consumer
  .apply(logger)
  .forRoutes(CatsController);
Copy the code

The global

INestApplication instances have a use() method:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Copy the code

Filters exception filter

What is the Filters

Nest comes with a built-in exception-handling layer that handles all unhandled exceptions throughout the application. When your code does not handle an exception, the layer catches the exception and sends a user-friendly response back.

The built-inHttpException

The Exception Filters mechanism is out-of-the-box and is performed by a built-in global Exception filter that handles exceptions for the HttpException class (and its subclasses). If an exception is not recognized (neither an HttpException nor a class inherited from HttpException), the built-in exception filter will generate the following default JSON response:

{
  "statusCode": 500,
  "message": "Internal server error"
}
Copy the code

Feel free to use the built-in HttpException capture:

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
Copy the code

The client will receive the following response:

{
  "statusCode": 403,
  "message": "Forbidden"
}
Copy the code

To override only the message part of the JSON response body, we simply supply a string in the Response parameter. To override the entire JSON response body, I can pass an object in the Response parameter. Such as:

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}
Copy the code

In this case, the response would look like this:

{
  "status": 403,
  "error": "This is a custom message"
}

Copy the code

Built-in HttpException subclasses are easy to use

Nest provides a set of exception Filters that inherit from HttpException, such as:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • .

So, the following code does the same:

 throw new HttpException('user not exist',HttpStatus.BAD_REQUEST)
 throw new BadRequestException('User not exist');
Copy the code

Why custom Filters

While the built-in Exception Filter can handle many situations automatically for you, you may want to have complete control over the Exception layer. For example, you might want to add logging or use other JSON schemas based on some dynamic factor. Exception filters are designed for this purpose. They allow you to grasp the precise flow of control and send the content of the response back to the client.

How do I customize a Filter

For example, we define a custom HttpExceptionFilter:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        message:exception.message,
        error:exception.name,
        timeStamp: new Date().toString(),
        path: request.url }); }}Copy the code

All ExceptionFilter handlers require the implements generic ExceptionFilter

interface, which requires you to implement the Catch (exception: T, host: ArgumentsHost) method. T represents the type of exception.

The client then receives an exception handling similar to this:

{ "statusCode": 401, "message": "Unauthorized", "error": "Error", "timeStamp": "Thu Jul 09 2020 11:38:07 GMT+0800 ", "path": "/user/aaa"}Copy the code

In the custom Filters code above, the @catch (HttpException) decorator binds the required metadata to the exception filter. It simply tells Nest that the filter is looking for an exception of type HttpException, nothing more complicated. The @catch () decorator can take a single argument or a comma-separated list. This allows you to set up filters for several types of exceptions simultaneously.

Arguments Host

Let’s look at the parameters of the catch() method. Exception is the exception object currently being handled, and the host argument is an ArgumentsHost object. ArgumentsHost is a powerful entity object. In this code example, we simply use it to get references to the Request and Response objects that were passed to the original Request processing (in the controller where the exception occurred). We use some helper methods at ArgumentsHost to get the required Request and Response objects. Learn more about ArgumentsHost in the next article section of execution.

Bind the Filters to the place where they are used

  • Method
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
Copy the code
  • Controller
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
Copy the code
  • Global
  1. useuseGlobalFilters
const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
Copy the code

UseGlobalInterceptors is used for each controller and each routes throughout the application. In terms of dependency injection, global interceptors registered from outside any module (such as using useGlobalInterceptors) cannot inject dependencies because this is done outside of all module contexts. To solve this problem, we can set the Interceptor directly in any module as follows.

  1. Use anyModule

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}
Copy the code

Pipes pipe

Pipe is made of@Injectable()Decorator annotated class. Pipelines should be implementedPipeTransformInterface.

Pipes is for what

  • Conversion: To convert input data to the desired format (for example, converting a string to an integer)
  • Validation: Validate input data, simply return it if it is correct, throw an exception if it is incorrect (such as the common validation of the user ID)

In both cases, the pipe operates on arguments handled by the Controller Route handler. Nest inserts a pipe before calling the method, and the pipe takes the parameters and acts on them. Any conversion/validation is done at this point, and then the Route Handler is called with the converted parameters.

For example, the ParseIntPipe pipe pipe converts a method handler parameter to a JavaScript integer (or throws an exception if the conversion fails).

The built-in Pipes

Nest gives us five pipes out of the box.

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe

How/where to use built-in pipes

Bind an instance of the pipe class to the appropriate context. In our example of ParseIntPipe, we want to associate the pipe with a particular route handling method and make sure it runs before that method is called. We use the following approach, which can be called binding pipes at the method parameter level:

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}
Copy the code

If we pass a string instead of a number, we get a response like this:

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}
Copy the code

This exception prevents the findOne method from executing.

Pass Pipe class or Pipe instance object

In the example above, we passed a class (ParseIntPipe) instead of an instance, leaving the instantiation to the framework and allowing dependency injection. Pipe, like Guard, we can pass instance objects. Passing live instance objects is useful if we want to enrich the behavior of built-in pipes with configuration options. Such as:

@Get(':id')
async findOne(
  @Param('id'.new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number.) {
  return this.catsService.findOne(id);
}
Copy the code

The exception status code we receive is 406 Not Acceptable:

{
    "statusCode": 406."message": "Validation failed (numeric string is expected)"."error": "Error"."timeStamp": "Thu Jul 09 2020 13:38:07 GMT+0800"."path": "/user/aaa"
}
Copy the code

Custom Pipes

For example, define a Pipe that verifies the user’s existence:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';

@Injectable(a)export class UserByIdPipe implements PipeTransform<string> {
    constructor(private readonly usersService: UsersService){}async transform(value: string, metadata: ArgumentMetadata) {
        const user = await this.usersService.findOne(value);
        if (user) {
            return user;
        } else {
            throw new BadRequestException('User not exist'); }}}Copy the code

PipeTransform <T, R> is a generic interface that must be implemented by any pipe. The generic interface uses T for the type of the input value and R for the return type of the transform () method.

Each pipe must implement the transform() method to implement the PipeTransform interface. This method takes two arguments:

  • value
  • metedata

The value argument is the method argument currently being processed, which is the value passed in. Metadata is the metadata of the method parameters being processed. Metadata objects have the following properties:

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom'; metatype? : Type<unknown>; data? :string;
}
Copy the code

Use custom pipes

 @Get(':id')
    getProfile(@Request() req, @Param('id', ParseIntPipe, UserByIdPipe) user: User) {
        return user;
    }

Copy the code

If the user exists, the user information is returned. If the user does not exist, the client receives this response:

{ "statusCode": 400, "message": "User not exist", "error": "Error", "timeStamp": "Thu Jul 09 2020 13:45:38 GMT+0800 ", "path": "/user/22"}Copy the code

Sometimes we don’t want to return user information directly, because we might want to verify before deleting/registering a user. In the pipe:

 if (user) {
            return value;
        } else {
            throw new BadRequestException('User not exist');
        }
Copy the code

How to use:

 @Delete(':id')
    remove(@Param('id', ParseIntPipe, UserByIdPipe) id: string): Promise<void> {
        return this.usersService.remove(id);
    }
Copy the code

Transform the usage scenario of the data

Validation is not the only scenario for custom pipes. Earlier we mentioned that pipes can also convert input data to the desired format, because the value returned from the transform method completely overrides the previous value of the parameter.

When do you use transformations? Sometimes the data passed from the client needs to be changed (for example, converting a string to an integer) before it can be properly processed through routing. In addition, some required data fields may be missing, and we want to use the default values. The transformation pipeline can do this by inserting processing logic between the client request and the request processing method.

The following example is a custom ParseIntPipe that parses strings into integer values. (As mentioned above, Nest has a built-in ParseIntPipe, which is more complex; We will use the following pipe only as a simple example of a custom transformation pipe.

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable(a)export class ParseIntPipe implements PipeTransform<string.number> {
  transform(value: string.metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    returnval; }}Copy the code

The default values fill the pipe

DefaultValuePipe DefaultValuePipe used

@Get(a)async findAll(
  @Query('activeOnly'.new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean.@Query('page'.new DefaultValuePipe(0), ParseIntPipe) page: number.) {
  return this.catsService.findAll({ activeOnly, page });
}

Copy the code

How to Use pipes

  • Parameter

    A pipeline of parameter ranges is useful when validation logic only cares about a particular parameter

    
    @Get(':id')
      async findOne(@Param('id', ParseIntPipe) id: number) {
        return this.catsService.findOne(id);
      }
    
    
     @Post(a)async create(
        @Body(new ValidationPipe()) createCatDto: CreateCatDto) {
           this.catsService.create(createCatDto);
      }
    Copy the code
  • Golbal

    1. useuseGlobalPipes
    const app = await NestFactory.create(AppModule);
    app.useGlobalPipes(new ValidationPipe());
    Copy the code
    1. Use anyModule
    import { Module } from '@nestjs/common';
    import { APP_PIPE } from '@nestjs/core';
    
    @Module({
       providers: [{provide: APP_PIPE,
          useClass: ValidationPipe,
        },
          ],
      })
     export class AppModule {}
    Copy the code

Guards Route Guard

What is the Guards

Guards have a single role. They determine whether a given request is handled by a routing handler based on certain conditions that occur at run time (such as permissions, roles, ACLs, and so on). This is often called authorization. Authorization (and what it usually works with — authentication) is typically handled by middleware in traditional Express applications. Middleware is a good choice for authentication because things like token validation, or adding attributes to request objects, do not have a strong association with routing.

However, middleware has other issues. After calling the next() function, it doesn’t know which handler will be executed. Guards have access to the ExecutionContext instance, so they know exactly what will be executed next. They are very similar in design to exception filters, pipes, and interceptors, allowing you to insert processing logic at the right place in the request/response cycle.

Guards execute after middleware and before interceptors/pipes.

How to implement Guards

As mentioned earlier, Authorization is a good use case for Guards, because specific routes are only available if the caller (usually a specific authenticated user) has sufficient permissions. For now, we will build the AuthGuard to assume that the authenticated user (the Token has been added to the request Headers) has been authenticated. It extracts and validates the token and uses the extracted information to determine whether the request can proceed.

For JWT Token Auth authentication, I will write an article later on how to implement it.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable(a)export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    returnvalidateRequest(request); }}Copy the code

Each Guards must implement the canActivate() function. This function should return a Boolean value indicating whether the current request is allowed. It can either synchronize or asynchronously (through promises or Observables) the return response. Nest uses the return value to control the next step:

  • If true is returned, the request is processed.
  • If false is returned, Nest will reject the request.

Execution context

The canActivate() function takes only one argument, an ExecutionContext instance. ExecutionContext inherits from ArgumentsHost. We saw ArgumentsHost in the exception filter above. In the example above, we simply use the same method defined on our earlier ArgumentsHost to get a reference to the Request object.

Through the ArgumentsHost inheritance, ExecutionContext also adds several methods that provide more details about the current execution process. This information helps us build Guards that work in the context of controller, method, and execution. Learn more about ExecutionContext here.

How to use Guards

  • Controller
    @Controller('cats')
    @UseGuards(RolesGuard)
    export class CatsController {}
    Copy the code
  • Method
    @UseGuards(RolesGuard)
    @Delete(':id')
    remove(@Param('id', ParseIntPipe, UserByIdPipe) id: string): Promise<void> {
          return this.usersService.remove(id);
    }
    Copy the code
  • Global
    1. useuseGlobalGuards
    const app = await NestFactory.create(AppModule);
    app.useGlobalGuards(new RolesGuard());
    Copy the code
    1. usemodule
    import { Module } from '@nestjs/common';
    import { APP_GUARD } from '@nestjs/core';
    
    @Module({
    providers: [
      {
        provide: APP_GUARD,
        useClass: RolesGuard,
      },
      ],
     })
     export class AppModule {}
    Copy the code

Set the role

Our RolesGuard works, but it has not yet taken advantage of the most important Guards feature, the Execution Context. It also does not know which roles each handler allows access to. For example, CatsController might have different permission schemes for different routes. Some may only be available to administrator users, while others may be available to everyone. How do we match roles to routes?

This is where custom metadata comes in (learn more). Nest provides the ability to attach custom metadata to a route via the @setmetadat () decorator. This metadata provides the role data that we are missing. Let’s take a look at using @setmetadata () :

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
Copy the code

We append the role metadata (roles is the metadata key and [‘admin’] is value) to the create() method. Instead of using @setmetadata () directly in a route, we can wrap it as an @role () decorator.

import { SetMetadata } from '@nestjs/common'; export const Roles = (... roles: string[]) => SetMetadata('roles', roles);Copy the code

Then inside the Controller:

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

Copy the code

Implement role-based Guards

According to the metadata role information of the route, we judge whether the user has the right to enter the route in the Guards:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable(a)export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {const roles = this.reflector.get<string[] > ('roles', context.getHandler());
    if(! roles) {return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () = >
      user.roles.some(role= >!!!!! roles.find(item= > item === role));

    returnuser && user.roles && hasRole(); }}Copy the code

Obtaining user information through request.user assumes that Token authentication has already been done in the application and user information is stored in the context, which is generally required for every application.

See the Reflection and Metadata section of Execution Context for more details on how to use Reflector in the following context.

In this case, if the user is not an admin user, they will receive the following response:

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}
Copy the code

The framework uses ForbiddenException to throw exceptions by default. Of course, if you don’t want to use the default, you can also throw your own exceptions inside Guards, such as:

throw new UnauthorizedException();
Copy the code

Interceptors interceptor

What does an interceptor do?

  • Add our own logic before/after method execution
  • Transform/wrap the result returned by a method
  • Cast/wrap an exception thrown by a method
  • Extend the content of the underlying method
  • Completely overwrite/override a method for special cases (such as the purpose of caching)

Using range

  • Controller
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
Copy the code
  • Method
 @UseInterceptors(LoggingInterceptor)
 @Get()
 async findAll(): Promise<Cat[]> {
   return this.catsService.findAll();
 }
Copy the code
  • Global
  1. useuseGlobalInterceptors
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
Copy the code
  1. Use any Module
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [{provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}
Copy the code

Custom Decorators Custom Decorators

What is a decorator

Nest is built around a language feature called decorator. The concept of decorators can be summarized simply as:

The ES6 decorator is an expression that returns a function, taking the target, name, and attribute description as parameters. You can apply a decorator by prefacing it with the @ character and placing it at the very top of the content to decorate. The content can be a class, method, or property.

How do I customize decorators

When the behavior of the decorator depends on certain conditions, you can use the data argument to pass the argument to the decorator’s factory function. A common scenario is a custom decorator that extracts properties from the request object by key. For example, the username field is passed to get the value. If the User object is complex, this will make it easier to get the data and more readable.

user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const User = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user && user[data] : user; });Copy the code

user.controller.ts

 @Get('username')
 async findOne(@UserDecorator('username') username: string) {
       return `Hello ${username}`;
}
Copy the code

Use with pipes

Nest treats custom decorators the same as the built-in ones, and we can use pipe validation and so on as well as the built-in ones.

@Get(a)async findOne(@User(new ValidationPipe()) user: UserEntity) {
  console.log(user);
}
Copy the code

Combination decorator

Nest provides an auxiliary way to compose multiple decorators. For example, suppose you want to combine all the authentication-related decorators into one decorator. This can be done in the following ways:

import { applyDecorators } from '@nestjs/common';

export function Auth(. roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized"'})); }Copy the code

Then inside the Controller:

@Get('users')
@Auth('admin')
findAllUsers() {}
Copy the code

FindAllUsers now has the effect of applying four decorators through a single declaration.

Designed by Kamil Mysliwiec, the author of the Nest framework, Nest is simple and easy to use, and the extension is flexible and powerful. But we need to understand what these Nest concepts are for so we know when to implement a Filter and when to implement a Pipe. Or know the difference between Middleware and Guards. Interceptor, in particular, seems to work just about anywhere. If you don’t understand the official documentation, it’s easy to misuse it.

I’ll update you later on more Nest tips, such as how to implement JWT Token authentication, how to design environment variable/global data registration, and other interesting things.

Old maodian community forest edge, the road turned brook bridge suddenly see. Nice to meet you, stranger.