Validation middleware

We may resort to a middleware when we cannot have automatic validation.

If you have noticed, we are not validating the login credentials in a DTO before we actually check if a user with that email exists and the password is correct. In this case, we unfortunately cannot use the @Body() decorator to validate the login credentials because guards are activated before pipes, and therefore the LocalAuthGuard is activated before the ValidationPipe. A solution we can use, although not as tidy as just using a DTO directly with the @Body() decorator, is to use a Middleware, which is the only thing that is activated before a guard. Let's then begin.

The official NestJS doc about Request lifecycle better explains this execution order.

First, we will actually create a DTO for the login credentials. Create then, the file auth/dto/login.dto with the respective validation.

@IsEmail()
readonly email: string;

@IsPassword()
readonly password: string;

Another approach we could use, is to make the LoginDto be a PickType of the CreateUserDto, as the fields with their respective validations are already there. It may be a bit cleaner but maybe also a bit more coupled solution. The decision is left to the reader.

export class LoginDto extends PickType(CreateUserDto, ['email', 'password'] as const) {}

After that, let's create the middleware.

nest g mi auth/middleware/login-validation

Remember to replace the types of the parameters with Request, Response and NextFunction from express.

Inside the use() method, first we'll transform the req.body into an instance of the LoginDto.

const loginDto = plainToInstance(LoginDto, req.body);

Then, we'll manually use the validate() method from class-validator to obtain potential errors from the validation of the loginDto. Notice that we also use options to ensure that no strange fields will be accepted.

const errors = await validate(loginDto, {
  whitelist: true,
  forbidNonWhitelisted: true,
});

Then, if there are errors, we send their messages in a BadRequestException. We want to obtain only the values (Object.values()) from the constraints field, and then merge the resulting arrays into a single array with the error messages (flatMap()).

if (errors.length) {
  const errorMessages = errors.flatMap((error) =>
    Object.values(error.constraints),
  );
  throw new BadRequestException(errorMessages);
}

All right, we have our LoginValidationMiddleware. To use it, we need to go to the AuthModule. First, make it implement the NestModule interface. Then, inside the class, implement the configure() method, which receives a MiddlewareConsumer. Make the consumer call the apply() method with the middleware to be used. Then, chain this with a call to the forRoutes() method, with the path to the route that will receive the middleware.

export class AuthModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoginValidationMiddleware).forRoutes('auth/login');
  }
}

And there we have it, our fields are being validated.

Commit - Validating login credentials with middleware

An improvement that could be done, is to create a generic ValidationMiddleware factory function that would have a DTO class as parameter, and return a validation middleware class responsible for validating that specific DTO. To achieve this, we should create a function that has a type parameter that extends the Type from NestJS (which represents a class), and a parameter of its type. After that, return the middleware class as it was, altering just the argument of validate() accordingly.

export const ValidationMiddleware = <TDto extends Type>(DtoClass: TDto) => {
  // Class goes here
}

We should also remember to rename the file itself and also the folder.

Commit - Generic middleware with factory function

Last updated