Pipes
A pipe is a class annotated with the @Injectable()
decorator, which implements the PipeTransform
interface.
Pipes have two typical use cases:
- transformation: transform input data to the desired form (e.g., from string to integer)
- validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception
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 and operates on them. Any transformation or validation operation takes place at that time, after which the route handler is invoked with any (potentially) transformed arguments.
Nest comes with a number of built-in pipes that you can use out-of-the-box. You can also build your own custom pipes. In this chapter, we'll introduce the built-in pipes and show how to bind them to route handlers. We'll then examine several custom-built pipes to show how you can build one from scratch.
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. This gives you a best-practice technique for validating data coming into the application from external sources at the system boundary.
Built-in pipes
Nest comes with nine pipes available out-of-the-box:
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
They're exported from the @nestjs/common
package.
Let's take a quick look at using ParseIntPipe
. This is an example of the transformation use case, where the pipe ensures that a method handler parameter is converted to a JavaScript integer (or throws an exception if the conversion fails). Later in this chapter, we'll show a simple custom implementation for a ParseIntPipe
. The example techniques below also apply to the other built-in transformation pipes (ParseBoolPipe
, ParseFloatPipe
, ParseEnumPipe
, ParseArrayPipe
and ParseUUIDPipe
, which we'll refer to as the Parse*
pipes in this chapter).
Binding pipes
To use a pipe, we need to bind an instance of the pipe class to the appropriate context. In our ParseIntPipe
example, we want to associate the pipe with a particular route handler method, and make sure it runs before the method is called. We do so with the following construct, which we'll refer to as binding the pipe at the method parameter level:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
This ensures that one of the following two conditions is true: either the parameter we receive in the findOne()
method is a number (as expected in our call to this.catsService.findOne()
), or an exception is thrown before the route handler is called.
For example, assume the route is called like:
GET localhost:3000/abc
Nest will throw an exception like this:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
The exception will prevent the body of the findOne()
method from executing.
In the example above, we pass a class (ParseIntPipe
), not an instance, leaving responsibility for instantiation to the framework and enabling dependency injection. As with pipes and guards, we can instead pass an in-place instance. Passing an in-place instance is useful if we want to customize the built-in pipe's behavior by passing options:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
Binding the other transformation pipes (all of the Parse* pipes) works similarly. These pipes all work in the context of validating route parameters, query string parameters and request body values.
For example with a query string parameter:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
Here's an example of using the ParseUUIDPipe
to parse a string parameter and validate if it is a UUID.
@@filename()
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
@@switch
@Get(':uuid')
@Bind(Param('uuid', new ParseUUIDPipe()))
async findOne(uuid) {
return this.catsService.findOne(uuid);
}
info Hint When using
ParseUUIDPipe()
you are parsing UUID in version 3, 4 or 5, if you only require a specific version of UUID you can pass a version in the pipe options.
Above we've seen examples of binding the various Parse*
family of built-in pipes. Binding validation pipes is a little bit different; we'll discuss that in the following section.
info Hint Also, see Validation techniques for extensive examples of validation pipes.
Custom pipes
As mentioned, you can build your own custom pipes. While Nest provides a robust built-in ParseIntPipe
and ValidationPipe
, let's build simple custom versions of each from scratch to see how custom pipes are constructed.
We start with a simple ValidationPipe
. Initially, we'll have it simply take an input value and immediately return the same value, behaving like an identity function.
@@filename(validation.pipe)
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class ValidationPipe {
transform(value, metadata) {
return value;
}
}
info Hint
PipeTransform<T, R>
is a generic interface that must be implemented by any pipe. The generic interface usesT
to indicate the type of the inputvalue
, andR
to indicate the return type of thetransform()
method.
Every pipe must implement the transform()
method to fulfill the PipeTransform
interface contract. This method has two parameters:
value
metadata
The value
parameter is the currently processed method argument (before it is received by the route handling method), and metadata
is the currently processed method argument's metadata. The metadata object has these properties:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
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,
|
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 beObject
.
Schema based validation
Let's make our validation pipe a little more useful. Take a closer look at the create()
method of the CatsController
, where we probably would like to ensure that the post body object is valid before attempting to run our service method.
@@filename()
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
async create(@Body() createCatDto) {
this.catsService.create(createCatDto);
}
Let's focus in on the createCatDto
body parameter. Its type is CreateCatDto
:
@@filename(create-cat.dto)
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
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 doing so is not ideal as it would break the single responsibility principle (SRP).
Another approach could be to create a validator class and delegate the task there. This has the disadvantage that we would have to remember to call this validator at the beginning of each method.
How about creating validation middleware? This could work, but unfortunately, it's not possible to create generic middleware which can be used across all contexts across the whole application. This is because middleware is unaware of the execution context, including the handler that will be called and any of its parameters.
This is, of course, exactly the use case for which pipes are designed. So let's go ahead and refine our validation pipe.