Skip to content
John Biundo edited this page Aug 25, 2019 · 2 revisions

Pipes Chapter

With this chapter, if you want to follow along, check out the pipes-start branch and go to the following along section.

If you just want a final version of the project as of the end of the chapter, check out the pipes-end branch, and proceed to the upon completion section.

Following along

Refer to the comments below, corresponding to chapter sections, to guide you through the code changes for nest cats as you proceed through the docs chapter.

Read through the Pipes chapter until you get to the Object schema validation section, then return here.

Object schema validation section

When you get to the Object schema validation section, go ahead and start with the npm package installs.

For the JoiValidationPipe class, we'll follow the convention we established in the last few chapters. Create a folder called pipes in the src/common folder. The folder structure should now look like:

nest-cats
└───src
    └───cats
    │   └───dto
    │   └───interfaces
    └───common
        └───filters
        └───middleware
        └───pipes

Go ahead and create joi-validation.pipe.ts in the src/common/pipes folder, using the code shown in this section of the docs.

Before moving on to the next section, let's take a few minutes to flesh the schema validation out further.

The docs are a little light on showing how to create a Joi schema that would be used with this validation pipe. Let's go ahead and create one now. Joi schemas are pretty straightforward; you can read all about them here.

Let's create a folder for the schema in the cats folder. You should end up with a folder called src/cats/schema. In it, create a file called create-cat.schema.ts. Add the following code to that file:

import * as Joi from '@hapi/joi';

export const createCatSchema = Joi.object().keys({
  name: Joi.string().required(),
  age: Joi.number()
    .integer()
    .min(0)
    .max(100)
    .required(),
  breed: Joi.string().required(),
});

Now you're ready to continue with the the Binding pipes documentation, here.

Binding pipes

Go ahead and bind the pipe to the create() method in src/cats/cats.controller.ts as shown in this section. As usual, you'll also need to import the other pieces: the schema, the UsePipes decorator, and the JoiValidationPipe class.

Test this with the following routes:

HTTPie routes for testing schema validation

add a valid cat

http POST :3000/cats name=Fred age:=3 breed='Alley Cat'

retrieve the valid cat

http GET :3000/cats

add a non-valid cat (we're missing required field breed)

http POST :3000/cats name=Fred age:=3

Note the error received:

{
  "error": "Bad Request",
  "message": "Validation failed",
  "statusCode": 400
}

Let's try to provide a better error. Write the validation error message to the console log by adding a console.log line to the transform method in the src/common/pipes/joi-validation.pipe.ts as shown below:

...
  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = Joi.validate(value, this.schema);
    if (error) {
      console.log('validation error: ', error);
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
...

Run the invalid POST /cats request again, and note the error logged to the console. It should look something like this

validation error: { ValidationError: child "breed" fails because ["breed" is required]
      ... (stack trace omitted)
   isJoi: true,
   name: 'ValidationError',
   details:
    [ { message: '"breed" is required',
        path: [ 'breed' ],
        type: 'any.required',
        context: { key: 'breed', label: 'breed' } } ],
  _object: { name: 'Fred', age: 3 },
  annotate: [Function] }

With this in hand, you can see how you could use the error.details property of the error returned by Joi.validate() to construct a more useful API response that would enable the front-end to show the user what was wrong with the input.

Class validator

When you get to the class validator section of the docs, go ahead and install the required dependencies.

Then go ahead and update the src/cats/dto/create-cat.dto.ts file as shown in the docs.

Then go ahead and create the ValidationPipe class in src/common/pipes/validation.pipe.ts.

To bind this validation pipe, follow the instructions in the docs, here near the end of the section, updating src/cats/cats.controller.ts. Since we already applied the schema validation pipe to the create() method, let's make a copy to work with. Copy/paste that method and comment out the first one. Then bind the new ValidationPipe as shown. Your code should look something like this:

  // Use one or the other of the following implementations for
  // the create() method.
  //
  // This version uses the JoiValidationPipe
  // @Post()
  // @UsePipes(new JoiValidationPipe(createCatSchema))
  // async create(@Body() createCatDto: CreateCatDto) {
  //   this.catsService.create(createCatDto);
  // }

  // This version uses the hand-crafted (class validator) ValidationPipe
  @Post()
  @UsePipes(new ValidationPipe())
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

As usual, you'll also need to import the newly created ValidationPipe into the src/cats/cats.controller.ts file as well.

HTTPie routes for testing class validator

As before:

add a valid cat

http POST :3000/cats name=Fred age:=3 breed='Alley Cat'

retrieve the valid cat

http GET :3000/cats

add a non-valid cat (we're missing required field breed)

http POST :3000/cats name=Fred age:=3

Note the error received:

{
  "error": "Bad Request",
  "message": "Validation failed",
  "statusCode": 400
}

Again, you can see how to provide a better error message by adding a console.log line to the transform method in the src/common/pipes/joi-validation.pipe.ts as shown below:

...
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      console.log('validation error: ', errors);
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
...

Run the invalid POST /cats request again, and note the error logged to the console. It should look something like this

validation error: [ ValidationError {
    target: CreateCatDto { name: 'Fred', age: 3 },
    value: undefined,
    property: 'breed',
    children: [],
    constraints: { isString: 'breed must be a string' } } ]

With this in hand, you can see how you could use the ValidationError property to construct a more useful API response that would enable the front-end to show the user what was wrong with the input.

Transformation use case section

When you get to the Transformation use case section of the docs, go ahead and create the parse-int.pipe.ts file in src/common/pipes, as shown.

Bind the pipe to the id parameter in the findOne() method of src/cats/cats.controller.ts, as shown in the docs. Don't forget to import your new ParseIntPipe class!

Note that the method in the docs assumes a findOne() method exists on CatsService. For now, we'll revert this to returning a static text message as we are only interested in testing our new ParseIntPipe. Change the implementation of that method to look like:

 @Get(':id')
  async findOne(@Param('id', new ParseIntPipe()) id) {
    return `This action returns a #${id} cat`;
  }
HTTPie requests to test the ParseIntPipe

make a valid request

http GET :3000/cats/1

make an invalid request - pass a string where an integer is expected

http GET :3000/cats/a

Again, note the generic error. Using the techniques shown earlier, you can construct a more helpful error message.

The built-in ValidationPipe section

When you get to the The built-in ValidationPipe section of the docs, go ahead and update the src/cats/cats.controller.ts file as shown to use this built-in pipe. Include the {transform: true} config object.

Since you now want to use the built-in ValidationPipe (which has the same name as the custom one we just built), make sure you remove (or comment out) the statement

import { ValidationPipe } from '../common/pipes/validation.pipe';

and then add ValidationPipe to the list of symbols imported from @nestjs/common

HTTPie routes for testing the built-in ValidationPipe

add a valid cat

http POST :3000/cats name=Fred age:=3 breed='Alley Cat'

retrieve the valid cat

http GET :3000/cats

add a non-valid cat (we're missing required field breed)

http POST :3000/cats name=Fred age:=3

Note the error received has specific information about how the validation failed:

{
  "error": "Bad Request",
  "message": [
    {
      "children": [],
      "constraints": {
        "isString": "breed must be a string"
      },
      "property": "breed",
      "target": {
        "age": 3,
        "name": "Fred"
      }
    }
  ],
  "statusCode": 400
}

Upon completion

We're done. We've tested our pipes as we went along.

What's next

Next up is the Guards chapter.