Skip to content

Combining Validators and Transformers in NestJS

When building a new API, it is imperative to validate that requests towards the API conform to a predefined specification or a contract.

For example, the specification may state that an input field must be a valid e-mail string. Or, the specification may state that one field is optional, while another field is mandatory. Although such validation can also be performed on the client side, we should never rely on it alone. There should always be a validation mechanism on the server side as well.

After all, you never know who's acting on behalf of the client. Therefore, you can never fully trust the data you receive. Popular backend frameworks usually have a very good support for validation out of the box, and NestJS, which we will cover in this blog post, is no exception.

In this blog post, we will be focusing on NestJS's validation using ValidationPipe- specifically on one lesser known feature- which is the ability to not only validate input, but transform it beforehand as well, thereby combining transformation and validation of data in one go.

Using ValidationPipe

To test this out, let's build a UsersController that supports getting a list of users, and with the option to filter by several conditions. After scaffolding our project using nest new [project-name], let's define a class that will represent this collection of filters, and name it GetUsersQuery:

class GetUsersQuery {
  userIds: string[];

  nameContains: string;

  pageSize: number;
}

Now, let's use it in the controller:

class User {
  id: string;
  name: string;
  active = false;
}

@Controller('users')
export class UsersController {
  @Get()
  getUsers(@Query() query: GetUsersQuery): User[] {
    console.log(JSON.stringify(query));
    return [
      { id: '1', name: 'Zeus Carver', active: true },
      { id: '2', name: 'Holly Gennero', active: true },
    ];
  }
}

The problem with this approach is that there is no validation performed whatsoever. Although we've defined userIds as an array of strings, and pageSize as a number, this is just compile-time verification - there is no runtime validation. In fact, if you execute a GET request on http://localhost:3000/users?userIds=1,2,3&pageSize=3, the query object will actually contain only string fields:

{
  "userIds": "1,2,3",
  "pageSize": "3"
}

There's a way to fix this in NestJS. First, let's install the dependencies needed for using data transformation and validation in NestJS:

npm i --save class-validator class-transformer

As their names would suggest, the class-validator package brings support for validating data, while the class-transformer package brings support for transforming data.

Each package adds some decorators of their own to aid you in this. For example, the class-validator package has the @IsNumber() decorator to perform runtime validation that a field is a valid number, while the class-transformer package has the @Type() decorator to perform runtime transformation from one type to another.

Having that in mind, let's decorate our GetUsersQuery a bit:

class GetUsersQuery {
  @IsArray()
  @IsOptional()
  @Transform(({ value }) => value.split(','))
  userIds: string[];

  @IsOptional()
  nameContains: string;

  @IsOptional()
  @IsNumber()
  @Type(() => Number)
  pageSize: number;
}

This is not enough, though. To utilize the class-validator decorators, we need to use the ValidationPipe. Additionally, to utilize the class-transformer decorators, we need to use ValidationPipe with its transform: true flag:

class User {
  id: string;
  name: string;
  active = false;
}

@Controller('users')
export class UsersController {
  @Get()
  @UsePipes(new ValidationPipe({ transform: true }))
  getUsers(@Query() query: GetUsersQuery): User[] {
    console.log(JSON.stringify(query));
    return [
      { id: '1', name: 'Zeus Carver', active: true },
      { id: '2', name: 'Holly Gennero', active: true },
    ];
  }
}

Here's what happens in the background. As said earlier, by default, every path parameter and query parameter comes over the network as a string. We could convert these values to their JavaScript primitives in the controller (array of strings and a number, respectively), or we can use the transform: true property of the ValidationPipe to do this automatically.

NestJS does need some guidance on how to do it, though. That's where class-transformer decorators come in. Internally, NestJS will use Class Transformer's plainToClass method to convert the above object to an instance of the GetUsersQuery class, using the Class Transformer decorators to transform the data along the way. After this, our object becomes:

{
  "userIds": ["1", "2", "3"],
  "pageSize": 3
}

Now, Class Validator comes in, using its annotations to validate that the data comes in as expected. Why is Class Validator needed if we already transformed the data beforehand? Well, Class Transformer will not throw any errors if it failed to transform the data. This means that, if you provided a string like "testPageSize" to the pageSize query parameter, our query object will actually come in as:

{
  "userIds": ["1", "2", "3"],
  "pageSize": null
}

And this is where Class Validator will kick in and raise an error that pageSize is not a proper number:

{
  "statusCode": 400,
  "message": [
    "pageSize must be a number conforming to the specified constraints"
  ],
  "error": "Bad Request"
}

Other transformation options

The @Type and @Transform decorators give us all kinds of options for transforming data. For example, strings can be converted to dates and then validated using the following combination of decorators:

@IsOptional()
@Type(() => Date)
@IsDate()
registeredSince: Date;

We can do the same for booleans:

@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive: boolean;

If we wanted to define advanced transformation rules, we can do so through an anonymous function passed to the @Transform decorator. With the following transformation, we can also accept isActive=1 in addition to isActive=true, and it will properly get converted to a boolean value:

@IsOptional()
@Transform(({ value }) => value === '1' || value === 'true')
@IsBoolean()
isActive: boolean;

Conclusion

This was an overview of the various options you have at your disposal when validating and transforming data. As you can see, NestJS gives you many options to declaratively define your validation and transformation rules, which will be enforced by ValidationPipe. This allows you to focus on your business logic in controllers and services, while being assured that the controller inputs have been properly validated.

You'll find the source code for this blog post's project on our GitHub.