Pipes

A pipe is a class annotated with the @Injectable() decorator. Pipes should implement the PipeTransform interface.

Pipes - 图1

Pipes have two typical use cases:

  • transformation: transform input data to the desired output
  • validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception when the data is incorrect

In both cases, pipes operate on the arguments being processed by a controller route handler. Nest interposes a pipe just before a method is invoked, and the pipe receives the arguments destined for the method. Any transformation or validation operation takes place at that time, after which the route handler is invoked with any (potentially) transformed arguments.

info Hint Pipes run inside the exceptions zone. This means that when a Pipe throws an exception it is handled by the exceptions layer (global exceptions filter and any exceptions filters that are applied to the current context). Given the above, it should be clear that when an exception is thrown in a Pipe, no controller method is subsequently executed.

Built-in pipes

Nest comes with three pipes available right out-of-the-box: ValidationPipe, ParseIntPipe and ParseUUIDPipe. They’re exported from the @nestjs/common package. In order to better understand how they work, let’s build them from scratch.

Let’s start with the ValidationPipe. Initially, we’ll have it simply take an input value and immediately return the same value, behaving like an identity function.

  1. @@filename(validation.pipe)
  2. import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
  3. @Injectable()
  4. export class ValidationPipe implements PipeTransform {
  5. transform(value: any, metadata: ArgumentMetadata) {
  6. return value;
  7. }
  8. }
  9. @@switch
  10. import { Injectable } from '@nestjs/common';
  11. @Injectable()
  12. export class ValidationPipe {
  13. transform(value, metadata) {
  14. return value;
  15. }
  16. }

info Hint PipeTransform<T, R> is a generic interface in which T indicates the type of the input value, and R indicates the return type of the transform() method.

Every pipe has to provide the transform() method. This method has two parameters:

  • value
  • metadata

The value is the currently processed argument (before it is received by the route handling method), while metadata is its metadata. The metadata object has these properties:

  1. export interface ArgumentMetadata {
  2. type: 'body' | 'query' | 'param' | 'custom';
  3. metatype?: Type<any>;
  4. data?: string;
  5. }

These properties describe the currently processed argument.

type Indicates whether the argument is a body @Body(), query @Query(), param @Param(), or a custom parameter (read more here).
metatype Provides the metatype of the argument, for example, String. Note: the value is undefined if you either omit a type declaration in the route handler method signature, or use vanilla JavaScript.
data The string passed to the decorator, for example @Body(‘string’). It’s undefined if you leave the decorator parenthesis empty.

warning Warning TypeScript interfaces disappear during transpilation. Thus, if a method parameter’s type is declared as an interface instead of a class, the metatype value will be Object.

Validation use case

Let’s take a closer look at the create() method of the CatsController.

  1. @@filename()
  2. @Post()
  3. async create(@Body() createCatDto: CreateCatDto) {
  4. this.catsService.create(createCatDto);
  5. }
  6. @@switch
  7. @Post()
  8. async create(@Body() createCatDto) {
  9. this.catsService.create(createCatDto);
  10. }

Let’s focus in on the createCatDto body parameter. Its type is CreateCatDto:

  1. @@filename(create-cat.dto)
  2. export class CreateCatDto {
  3. readonly name: string;
  4. readonly age: number;
  5. readonly breed: string;
  6. }

We want to ensure that any incoming request to the create method contains a valid body. So we have to validate the three members of the createCatDto object. We could do this inside the route handler method, but we would break the single responsibility rule (SRP). Another approach could be to create a validator class and delegate the task there, but we would have to use this validator at the beginning of each method. How about creating a validation middleware? This could be a good idea, but it’s not possible to create generic middleware which can be used across the whole application (because middleware is unaware of the execution context, including the handler that will be called and any of its parameters).

It turns out that this is a case ideally suited for a Pipe. So let’s go ahead and build one.

Object schema validation

There are several approaches available for object validation. One common approach is to use schema-based validation. The Joi library allows you to create schemas in a pretty straightforward way, with a readable API. Let’s look at a pipe that makes use of Joi-based schemas.

Start by installing the required package:

  1. $ npm install --save @hapi/joi
  2. $ npm install --save-dev @types/hapi__joi

In the code sample below, we create a simple class that takes a schema as a constructor argument. We then apply the schema.validate() method, which validates our incoming argument against the provided schema.

As noted earlier, a validation pipe either returns the value unchanged, or throws an exception.

In the next section, you’ll see how we supply the appropriate schema for a given controller method using the @UsePipes() decorator.

  1. @@filename()
  2. import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
  3. @Injectable()
  4. export class JoiValidationPipe implements PipeTransform {
  5. constructor(private readonly schema: Object) {}
  6. transform(value: any, metadata: ArgumentMetadata) {
  7. const { error } = this.schema.validate(value);
  8. if (error) {
  9. throw new BadRequestException('Validation failed');
  10. }
  11. return value;
  12. }
  13. }
  14. @@switch
  15. import { Injectable, BadRequestException } from '@nestjs/common';
  16. @Injectable()
  17. export class JoiValidationPipe {
  18. constructor(schema) {
  19. this.schema = schema;
  20. }
  21. transform(value, metadata) {
  22. const { error } = this.schema.validate(value);
  23. if (error) {
  24. throw new BadRequestException('Validation failed');
  25. }
  26. return value;
  27. }
  28. }

Binding pipes

Binding pipes (tying them to the appropriate controller or handler) is very straightforward. We use the @UsePipes() decorator and create a pipe instance, passing it a Joi validation schema.

  1. @@filename()
  2. @Post()
  3. @UsePipes(new JoiValidationPipe(createCatSchema))
  4. async create(@Body() createCatDto: CreateCatDto) {
  5. this.catsService.create(createCatDto);
  6. }
  7. @@switch
  8. @Post()
  9. @Bind(Body())
  10. @UsePipes(new JoiValidationPipe(createCatSchema))
  11. async create(createCatDto) {
  12. this.catsService.create(createCatDto);
  13. }

Class validator

warning Warning The techniques in this section require TypeScript, and are not available if your app is written using vanilla JavaScript.

Let’s look at an alternate implementation of our validation technique.

Nest works well with the class-validator library. This amazing library allows you to use decorator-based validation. Decorator-based validation is extremely powerful, especially when combined with Nest’s Pipe capabilities since we have access to the metatype of the processed property. Before we start, we need to install the required packages:

  1. $ npm i --save class-validator class-transformer

Once these are installed, we can add a few decorators to the CreateCatDto class.

  1. @@filename(create-cat.dto)
  2. import { IsString, IsInt } from 'class-validator';
  3. export class CreateCatDto {
  4. @IsString()
  5. readonly name: string;
  6. @IsInt()
  7. readonly age: number;
  8. @IsString()
  9. readonly breed: string;
  10. }

Info Hint Read more about the class-validator decorators here.

Now we can create a ValidationPipe class.

  1. @@filename(validation.pipe)
  2. import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
  3. import { validate } from 'class-validator';
  4. import { plainToClass } from 'class-transformer';
  5. @Injectable()
  6. export class ValidationPipe implements PipeTransform<any> {
  7. async transform(value: any, { metatype }: ArgumentMetadata) {
  8. if (!metatype || !this.toValidate(metatype)) {
  9. return value;
  10. }
  11. const object = plainToClass(metatype, value);
  12. const errors = await validate(object);
  13. if (errors.length > 0) {
  14. throw new BadRequestException('Validation failed');
  15. }
  16. return value;
  17. }
  18. private toValidate(metatype: Function): boolean {
  19. const types: Function[] = [String, Boolean, Number, Array, Object];
  20. return !types.includes(metatype);
  21. }
  22. }

warning Notice Above, we have used the class-transformer library. It’s made by the same author as the class-validator library, and as a result, they play very well together.

Let’s go through this code. First, note that the transform() function is async. This is possible because Nest supports both synchronous and asynchronous pipes. We do this because some of the class-validator validations can be async (utilize Promises).

Next note that we are using destructuring to extract the metatype field (extracting just this member from an ArgumentMetadata) into our metatype parameter. This is just shorthand for getting the full ArgumentMetadata and then having an additional statement to assign the metatype variable.

Next, note the helper function toValidate(). It’s responsible for bypassing the validation step when the current argument being processed is a native JavaScript type (these can’t have schemas attached, so there’s no reason to run them through the validation step).

Next, we use the class-transformer function plainToClass() to transform our plain JavaScript argument object into a typed object so that we can apply validation. The incoming body, when deserialized from the network request, does not have any type information. Class-validator needs to use the validation decorators we defined for our DTO earlier, so we need to perform this transformation.

Finally, as noted earlier, since this is a validation pipe it either returns the value unchanged, or throws an exception.

The last step is to bind the ValidationPipe. Pipes, similar to exception filters, can be method-scoped, controller-scoped, or global-scoped. Additionally, a pipe can be param-scoped. In the example below, we’ll directly tie the pipe instance to the route param @Body() decorator.

  1. @@filename(cats.controller)
  2. @Post()
  3. async create(
  4. @Body(new ValidationPipe()) createCatDto: CreateCatDto,
  5. ) {
  6. this.catsService.create(createCatDto);
  7. }

Param-scoped pipes are useful when the validation logic concerns only one specified parameter.

Alternatively, to set up a pipe at a method level, use the @UsePipes() decorator.

  1. @@filename(cats.controller)
  2. @Post()
  3. @UsePipes(new ValidationPipe())
  4. async create(@Body() createCatDto: CreateCatDto) {
  5. this.catsService.create(createCatDto);
  6. }

info Hint The @UsePipes() decorator is imported from the @nestjs/common package.

In the example above, an instance of ValidationPipe has been created immediately in-place. Alternatively, pass the class (not an instance), thus leaving instantiation up to the framework, and enabling dependency injection.

  1. @@filename(cats.controller)
  2. @Post()
  3. @UsePipes(ValidationPipe)
  4. async create(@Body() createCatDto: CreateCatDto) {
  5. this.catsService.create(createCatDto);
  6. }

Since the ValidationPipe was created to be as generic as possible, let’s set it up as a global-scoped pipe, applied to every route handler across the entire application.

  1. @@filename(main)
  2. async function bootstrap() {
  3. const app = await NestFactory.create(AppModule);
  4. app.useGlobalPipes(new ValidationPipe());
  5. await app.listen(3000);
  6. }
  7. bootstrap();

warning Notice In the case of hybrid apps the useGlobalPipes() method doesn’t set up pipes for gateways and micro services. For “standard” (non-hybrid) microservice apps, useGlobalPipes() does mount pipes globally.

Global pipes are used across the whole application, for every controller and every route handler. In terms of dependency injection, global pipes registered from outside of any module (with useGlobalPipes() as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can set up a global pipe directly from any module using the following construction:

  1. @@filename(app.module)
  2. import { Module } from '@nestjs/common';
  3. import { APP_PIPE } from '@nestjs/core';
  4. @Module({
  5. providers: [
  6. {
  7. provide: APP_PIPE,
  8. useClass: ValidationPipe,
  9. },
  10. ],
  11. })
  12. export class AppModule {}

info Hint When using this approach to perform dependency injection for the pipe, note that regardless of the module where this construction is employed, the pipe is, in fact, global. Where should this be done? Choose the module where the pipe (ValidationPipe in the example above) is defined. Also, useClass is not the only way of dealing with custom provider registration. Learn more here.

Transformation use case

Validation isn’t the sole use case for Pipes. At the beginning of this chapter, we mentioned that a pipe can also transform the input data to the desired output. This is possible because the value returned from the transform function completely overrides the previous value of the argument. When is this useful? Consider that sometimes the data passed from the client needs to undergo some change - for example converting a string to an integer - before it can be properly handled by the route handler method. Furthermore, some required data fields may be missing, and we would like to apply default values. Transformer pipes can perform these functions by interposing a processing function between the client request and the request handler.

Here’s a ParseIntPipe which is responsible for parsing a string into an integer value.

  1. @@filename(parse-int.pipe)
  2. import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
  3. @Injectable()
  4. export class ParseIntPipe implements PipeTransform<string, number> {
  5. transform(value: string, metadata: ArgumentMetadata): number {
  6. const val = parseInt(value, 10);
  7. if (isNaN(val)) {
  8. throw new BadRequestException('Validation failed');
  9. }
  10. return val;
  11. }
  12. }
  13. @@switch
  14. import { Injectable, BadRequestException} from '@nestjs/common';
  15. @Injectable()
  16. export class ParseIntPipe {
  17. transform(value, metadata) {
  18. const val = parseInt(value, 10);
  19. if (isNaN(val)) {
  20. throw new BadRequestException('Validation failed');
  21. }
  22. return val;
  23. }
  24. }

We can simply tie this pipe to the selected param as shown below:

  1. @@filename()
  2. @Get(':id')
  3. async findOne(@Param('id', new ParseIntPipe()) id) {
  4. return await this.catsService.findOne(id);
  5. }
  6. @@switch
  7. @Get(':id')
  8. @Bind(Param('id', new ParseIntPipe()))
  9. async findOne(id) {
  10. return await this.catsService.findOne(id);
  11. }

If you prefer you can use the ParseUUIDPipe which is responsible for parsing a string and validate if is a UUID.

  1. @@filename()
  2. @Get(':id')
  3. async findOne(@Param('id', new ParseUUIDPipe()) id) {
  4. return await this.catsService.findOne(id);
  5. }
  6. @@switch
  7. @Get(':id')
  8. @Bind(Param('id', new ParseUUIDPipe()))
  9. async findOne(id) {
  10. return await this.catsService.findOne(id);
  11. }

info Hint When using ParseUUIDPipe() you are parsing UUID in version 3, 4 or 5, if you only requires a specific version of UUID you can pass a version in the pipe options.

With this in place, ParseIntPipe or ParseUUIDPipe will be executed before the request reaches the corresponding handler, ensuring that it will always receive an integer or uuid (according on the used pipe) for the id parameter.

Another useful case would be to select an existing user entity from the database by id:

  1. @@filename()
  2. @Get(':id')
  3. findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  4. return userEntity;
  5. }
  6. @@switch
  7. @Get(':id')
  8. @Bind(Param('id', UserByIdPipe))
  9. findOne(userEntity) {
  10. return userEntity;
  11. }

We leave the implementation of this pipe to the reader, but note that like all other transformation pipes, it receives an input value (an id) and returns an output value (a UserEntity object). This can make your code more declarative and DRY by abstracting boilerplate code out of your handler and into a common pipe.

The built-in ValidationPipe

Fortunately, you don’t have to build these pipes on your own since the ValidationPipe and the ParseIntPipe are provided by Nest out-of-the-box. (Keep in mind that ValidationPipe requires both class-validator and class-transformer packages to be installed).

The built-in ValidationPipe offers more options than in the sample we built in this chapter, which has been kept basic for the sake of illustrating the basic mechanics of a pipe. You can find lots of examples here.

One such option is transform. Recall the earlier discussion about deserialized body objects being vanilla JavaScript objects (i.e., not having our DTO type). So far, we’ve used the pipe to validate our payload. You may recall that in the process, we used class-transform to temporarily convert our plain object into a typed object so that we could do the validation. The built-in ValidationPipe can also, optionally, return this converted object. We enable this behavior by passing in a configuration object to the pipe. For this option, pass a config object with the field transform with a value true as shown below:

  1. @@filename(cats.controller)
  2. @Post()
  3. @UsePipes(new ValidationPipe({ transform: true }))
  4. async create(@Body() createCatDto: CreateCatDto) {
  5. this.catsService.create(createCatDto);
  6. }

info Hint The ValidationPipe is imported from the @nestjs/common package.

Because this pipe is based on the class-validator and class-transformer libraries, there are many additional options available. Like the transform option above, you configure these settings via a configuration object passed to the pipe. Following are the built-in options:

  1. export interface ValidationPipeOptions extends ValidatorOptions {
  2. transform?: boolean;
  3. disableErrorMessages?: boolean;
  4. exceptionFactory?: (errors: ValidationError[]) => any;
  5. }

In addition to these, all class-validator options (inherited from the ValidatorOptions interface) are available:

Option Type Description
skipMissingProperties boolean If set to true, validator will skip validation of all properties that are missing in the validating object.
whitelist boolean If set to true, validator will strip validated (returned) object of any properties that do not use any validation decorators.
forbidNonWhitelisted boolean If set to true, instead of stripping non-whitelisted properties validator will throw an exception.
forbidUnknownValues boolean If set to true, attempts to validate unknown objects fail immediately.
disableErrorMessages boolean If set to true, validation errors will not be returned to the client.
exceptionFactory Function Takes an array of the validation errors and returns an exception object to be thrown.
groups string[] Groups to be used during validation of the object.
dismissDefaultMessages boolean If set to true, the validation will not use default messages. Error message always will be undefined if its not explicitly set.
validationError.target boolean Indicates if target should be exposed in ValidationError
validationError.value boolean Indicates if validated value should be exposed in ValidationError.

info Notice Find more information about the class-validator package in its repository.