Pipes

Pipes are really useful feature. You should think about them as a streams of data. They're called immediately before route handlers.

How does it look?

Pipe is a simple class, which is decorated by @Pipe() and implements PipeTransform interface.

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

@Pipe()
export class CustomPipe implements PipeTransform {
    public transform(value, metadata: ArgumentMetadata) {
        return value;
    }
}

It has a one method, which receives 2 arguments:

  • value (any)
  • metadata (ArgumentMetadata)

metadata is the metadata of an argument for which the pipe is being processed, and the value is... just its value. The metadata holds few properties:

export interface ArgumentMetadata {
    type: 'body' | 'query' | 'param';
    metatype?: any;
    data?: any;
}

I'll tell you about them later.

How does it work?

Let's imagine we have a route handler:

@Post()
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

There is a request body parameter user. The type is UserDto:

export class UserDto {
    public readonly name: string;
    public readonly age: number;
}

This object always has to be correct, so we have to validate those two fields. We could do it in the route handler method, but we will break the single responsibility rule.

The second idea is to create validator class and delegate the task there, but we will have to call this validator at the beginning of the method every time.

So maybe should we create a validation middleware? It's good idea, but it's almost impossible to create a generic middleware, which might by used along entire application.

This is the first use-case, when you should consider to use a Pipe.

ValidatorPipe with Joi

There is an amazing library, which name is Joi. It's commonly used with hapi. Let's create a schema:

const userSchema = Joi.object().keys({
    name: Joi.string().alphanum().min(3).max(30).required(),
    age: Joi.number().min(1).required(),
}).required();

And the appropriate Pipe:

@Pipe()
export class JoiValidatorPipe implements PipeTransform {
    constructor(private readonly schema: Joi.ObjectSchema) {}

    public transform(value, metadata: ArgumentMetadata) {
        const { error } = Joi.validate(value, this.schema);
        if (error) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }
}

Notice! If you want to use your domain exception instead of built-in HttpException, you have to set-up Exception Filter.

Now, we only have to bind JoiValidatorPipe to our method.

@Post()
@UsePipes(new JoiValidatorPipe(userSchema))
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

The transform() function will be evaluated for each @Body(), @Param() and @Query() argument of the route handler. If you want to run validation only for @Body() arguments - use metadata properties. Let's create pipe with predicate:

@Pipe()
export class JoiValidatorPipe implements PipeTransform {
    constructor(
        private readonly schema: Joi.ObjectSchema,
        private readonly toValidate = (metadata: ArgumentMetadata) => true) {}

    public transform(value, metadata: ArgumentMetadata) {
        if (!this.toValidate(metadata)) {
            return value;
        }
        const { error } = Joi.validate(value, this.schema);
        if (error) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }
}

And the usage example:

@Post()
@UsePipes(new JoiValidatorPipe(userSchema, ({ type }) => type === 'body'))
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

Also, you can directly bind pipe to chosen argument:

@Post()
public async addUser(
    @Res() res: Response,
    @Body('user', new JoiValidatorPipe(userSchema)) user: UserDto) {

    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

That's all.

Joi is a great library, but this solution is not generic. We always have to create a schema, and the pipe instance. Can we make it better? Sure, we can.

ValidatorPipe with class-validator

There is an amazing library, which name is class-validator (great library @pleerock!). It allows to use decorator-based validation.

Let's create a generic validation pipe:

import { validate } from 'class-validator';

@Pipe()
export class ValidatorPipe implements PipeTransform {
    public async transform(value, metadata: ArgumentMetadata) {
        const { metatype } = metadata;
        if (!this.toValidate(metatype)) {
            return value;
        }
        const object = Object.assign(new metatype(), value);
        const errors = await validate(object);
        if (errors.length > 0) {
            throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST);
        }
        return value;
    }

    private toValidate(metatype = null): boolean {
        const types = [String, Boolean, Number, Array, Object];
        return !types.find((type) => metatype === type);
    }
}

IMPORTANT! It works only with TypeScript.

As you can see, the metatype property of ArgumentMetadata holds type of the value. Furthermore, the pipes can by asynchronous. When we would use ValidatorPipe there:

@Post()
@UsePipes(new ValidatorPipe())
public async addUser(@Res() res: Response, @Body('user') user: UserDto) {
    const msg = await this.usersService.addUser(user);
    res.status(HttpStatus.CREATED).json(msg);
}

The metatype will be UserDto.

That's it. We have a generic solution now.

Notice! It doesn't work with TypeScript interfaces - you have to use classes instead.

Scopes

You already know that pipes can be argument-scoped and method-scoped. It's not everything!

We can set-up pipe for each route handler in the Controller (controller-scoped pipes):

@Controller('users')
@UsePipes(new ValidatorPipe())
export class UsersController {}

Moreover, you can set-up global pipe for each route handler in the Nest application!

const app = NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidatorPipe());

This is how you can e.g. enable request properties auto-validation.

results matching ""

    No results matching ""