Photo by chansu shin on Unsplash

On this page

This chapter follows Interceptors in NestJS.

Every value that comes off the wire is untrusted and, in the case of route parameters and query strings, a string. A pipe sits between the request and one handler argument and does one of two things: transform the value into what the handler expects, such as "42" into 42, or validate it and throw when it does not fit. Either way, the handler can trust its arguments, and a rejected request never reaches it.

Built-in pipes

@nestjs/common ships pipes for the common conversions:

Pipe Turns the input into
ParseIntPipe, ParseFloatPipe A number
ParseBoolPipe A boolean from "true" or "false"
ParseUUIDPipe A validated UUID string
ParseEnumPipe A member of a given enum
ParseArrayPipe An array, optionally with typed items
ParseDatePipe A Date
DefaultValuePipe A fallback when the value is missing
ValidationPipe A validated DTO

They bind as the second argument of a parameter decorator. Pipes run left to right, so DefaultValuePipe goes first. The Parse* pipes reject undefined.

src/cats/cats.controller.ts
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
) {
return this.catsService.findAll({ page, activeOnly });
}
@Get(':id')
findOne(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.catsService.findOne(id);
}

A failed parse answers 400 Bad Request with a message such as Validation failed (numeric string is expected). Passing an instance, as with ParseUUIDPipe above, lets you set options such as errorHttpStatusCode.

Validating bodies with ValidationPipe

For request bodies, ValidationPipe checks the incoming JSON against the decorators on the argument’s DTO class. It relies on two libraries:

Terminal window
pnpm add class-validator class-transformer

Register it once for the whole application. The three options below are worth turning on in almost every project:

src/app.module.ts
import { ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
},
],
})
export class AppModule {}
  • whitelist strips properties that have no validation decorator on the DTO, so a client cannot slip an isAdmin field through to the database.
  • forbidNonWhitelisted rejects such a request instead of silently stripping it.
  • transform turns the plain body into an instance of the DTO class and converts primitive route parameters to their declared types.

With the DTO from Controllers in NestJS, a body of { "name": "Tom", "age": -1 } now fails before the handler runs:

{
"message": ["age must not be less than 0", "breed must be a string"],
"error": "Bad Request",
"statusCode": 400
}

Nested objects need @ValidateNested() together with @Type(() => AddressDto) from class-transformer, otherwise the nested object is not checked.

Writing a custom pipe

A pipe is a provider that implements PipeTransform. Its transform() method receives the value and an ArgumentMetadata object that says where the value came from (body, query, param, or custom) and what type the handler declared. It returns the new value or throws.

src/common/parse-slug.pipe.ts
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ParseSlugPipe implements PipeTransform<string, string> {
transform(value: string): string {
const slug = value.trim().toLowerCase();
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
throw new BadRequestException(`"${value}" is not a valid slug`);
}
return slug;
}
}

A pipe that only validates returns the value unchanged. The interface is the same for both jobs; there is no separate validation interface. Teams that prefer schema libraries over decorators write one small pipe around zod or joi the same way, calling the schema’s parse() in transform().

Where to bind pipes

Pipes bind at four levels, and all levels run: global, then controller (@UsePipes() on the class), then route (@UsePipes() on the method), then parameter.

Prefer the APP_PIPE provider shown above over app.useGlobalPipes() in main.ts. Both apply to every route, but only the provider is part of the module graph. An end-to-end test that builds the app from AppModule gets the provider automatically, while a pipe registered in main.ts has to be added again in the test setup. Testing NestJS applications shows the difference.

DTOs and the service layer

The DTO describes what a client may send. It carries validation decorators and is shaped by the HTTP contract. The service often needs something slightly different: an ID from the URL, the authenticated user’s ID, a timestamp. Two common layouts:

  • One DTO: the service accepts the validated DTO and adds the extra fields itself. Simple, and enough for most CRUD.
  • A DTO and a service input type: the controller maps the DTO and the request context into a plain CreateCatInput type. The service no longer depends on HTTP and can be called from a queue consumer or a CLI command.

Start with one DTO, and split when a second caller of the service appears or the two shapes drift apart.

Next: Exception filters in NestJS.

Check your understanding

Question 1 of 5

0 of 5 checked

  1. 1. Why does DefaultValuePipe come before ParseIntPipe in @Query('page', new DefaultValuePipe(1), ParseIntPipe)?

Search articles

Type a word to search published articles.

Open the search page