We use cookies to enhance your experience on the site
CodeWorlds

ValidationPipe - the automatic checkpoint

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, @name:

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

Go to CodeWorlds