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