NestJS course Β· Module 10: Data Validation

ValidationPipe - the automatic checkpoint

5 min read
In this lesson5

The decorators of the previous lessons - @IsString, @IsNumber, @Min - check nothing by themselves. They are a description of requirements pinned to fields, like a manifest pinned to a crate. Somebody must take that manifest in hand and compare it with the contents.

The main role of ValidationPipe is automatically running class-validator decorator validation. It does not encrypt data in HTTP requests, does not compress server responses, and does not generate Swagger documentation. It does one thing: it takes the incoming object, reads the decorators off the DTO class, and checks whether the data answers to them.

Global registration

The pipe is registered once, at application startup:

1async function bootstrap() {
2  const app = await NestFactory.create(AppModule);
3
4  app.useGlobalPipes(
5    new ValidationPipe({
6      whitelist: true,
7      forbidNonWhitelisted: true,
8      transform: true,
9    }),
10  );
11
12  await app.listen(3000);
13}

The notation reads in four parts: app is the application object, .useGlobalPipes( opens the registration, new ValidationPipe({ ... }) creates the pipe with its options, and ) closes the call.

Note the plural in the method's name: useGlobalPipes accepts several pipes separated by commas. In practice you almost always pass one - ValidationPipe - because the others, such as ParseIntPipe, only make sense on particular parameters.

Four modes, from the mildest

The pipe's configuration is not a set of independent switches but a ladder of strictness. It is worth knowing whole, because choosing a rung is a decision about how your API treats unknown data.

  1. No ValidationPipe - no validation. Any JSON object reaches the handler. Type annotations in the code are then a wish, not a guarantee.
  2. ValidationPipe() - basic validation. The decorators' rules apply, but fields outside the DTO pass through untouched.
  3. whitelist: true - removing unknown fields.
  4. whitelist + forbidNonWhitelisted - rejecting requests.

One thing separates the last two, and it is worth remembering exactly. The whitelist: true option automatically removes fields without validation decorators from incoming data. It does not add a white background to responses, does not create a list of allowed IP addresses, and does not disable validation on chosen endpoints - it simply cuts out whatever you did not describe in the DTO.

The difference from forbidNonWhitelisted: true is that whitelist removes unknown fields silently, whereas forbidNonWhitelisted returns a 400 error. The two are not identical, forbidNonWhitelisted disables nothing, and it adds no fields to the server logs.

1// The DTO describes only: name, cohortId
2// The client sends: { name: 'Marcus', cohortId: 3, isAdmin: true }
3
4// ValidationPipe()                     -> the service receives isAdmin: true
5// whitelist: true                      -> the service receives { name, cohortId }
6// whitelist + forbidNonWhitelisted     -> 400 Bad Request

The isAdmin field in that example is no accident. Without whitelist it reaches the service, and if somewhere further along somebody writes Object.assign(user, dto), the client has just granted itself privileges. It is the oldest way of taking over an account in applications that trust their input.

Which mode to choose? forbidNonWhitelisted is stricter and by that token more honest: the client learns it sent something unexpected, instead of assuming you saved its field. Plain whitelist can be more convenient on a public API where older clients still send fields since withdrawn.

transform

The last option of the production set concerns types. transform: true makes the handler receive an instance of the DTO class rather than a plain object - and converts simple types along the way. Without it @Param('id') id: number gives you the string '7', though the type says number, because everything arrives from a URL as text.

It is also the condition for @Type and @Transform from class-transformer to work: without transform: true those remain annotations with no effect.

On a single endpoint

When one endpoint needs different rules from the rest, the pipe is attached locally through @UsePipes(new ValidationPipe({ ... })) above the method. A local pipe takes precedence over the global one.

Reach for this rarely. Validation configuration scattered across controllers soon stops being legible, and answering "does this endpoint reject extra fields?" begins to require reading three files.

Summary

A manifest checks nothing by itself:

  • the main role of ValidationPipe is automatically running class-validator decorator validation - not encryption, not compression, not generating Swagger,
  • global registration: app β†’ .useGlobalPipes( β†’ new ValidationPipe({ ... }) β†’ ),
  • the modes from the least restrictive: no ValidationPipe β†’ ValidationPipe() β†’ whitelist: true β†’ whitelist + forbidNonWhitelisted,
  • whitelist: true removes fields without validation decorators from incoming data - it has nothing to do with IP addresses or disabling validation,
  • whitelist removes unknown fields silently, forbidNonWhitelisted returns a 400 error,
  • an unremoved field such as isAdmin is the simplest road to granting yourself privileges,
  • transform: true yields a DTO instance and converts simple types; without it @Type and @Transform do nothing,
  • @UsePipes above a method overrides the global configuration - use it sparingly.

In the next lesson we take up class-transformer - what happens to data in the other direction, on its way out of the API. For now remember: decorators describe, the pipe executes. Without it you have very precise documentation of rules nobody enforces.

Code for this lesson: src/validation-pipe.ts
1// ValidationPipe - Control station configuration
2import { NestFactory } from '@nestjs/core';
3import { ValidationPipe } from '@nestjs/common';
4
5// TODO: Configure ValidationPipe globally
6async function bootstrap() {
7  const app = await NestFactory.create(AppModule);
8
9  // TODO: Add app.useGlobalPipes with ValidationPipe
10  // Required options:
11  // - whitelist: true
12  // - forbidNonWhitelisted: true
13  // - transform: true
14
15  await app.listen(3000);
16  console.log('Imperium gate open on port 3000');
17}
18
19// Example of per-route usage
20import { Controller, Post, Body, UsePipes } from '@nestjs/common';
21import { IsString, IsNumber, IsNotEmpty, Min } from 'class-validator';
22
23class CreateSoldierDto {
24  @IsString()
25  @IsNotEmpty()
26  name: string;
27
28  @IsNumber()
29  @Min(16)
30  age: number;
31}
32
33@Controller('soldiers')
34class SoldierController {
35  // TODO: Add @UsePipes(new ValidationPipe({ whitelist: true }))
36  @Post()
37  create(@Body() dto: CreateSoldierDto) {
38    return { message: 'Soldier recruited!', data: dto };
39  }
40}
41
42console.log('ValidationPipe configured!');
43

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the main role of ValidationPipe in NestJS?

  2. 2. What does the whitelist: true option do in ValidationPipe?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Complete the ValidationPipe configuration in the bootstrap function

  • Horizontal ordering

    Arrange the elements of the global ValidationPipe configuration in the correct order:

  • Code editor

    Complete the comments describing the behavior of each ValidationPipe mode

  • Vertical ordering

    Arrange the ValidationPipe modes from least to most restrictive:

Useful articles