DEV Community

Cover image for NestJS ValidationPipe: Ensuring Secure Input Contracts
Ítalo Queiroz
Ítalo Queiroz

Posted on

NestJS ValidationPipe: Ensuring Secure Input Contracts

In modern API development, ensuring that input data is in the correct format and valid is fundamental for application security and stability. NestJS offers an elegant and powerful solution for this need through the ValidationPipe, a built-in pipe that works in conjunction with validation decorators to create robust input contracts.


What is the ValidationPipe?

The ValidationPipe is a global NestJS pipe that automatically validates input data based on validation metadata defined through decorators. It uses the class-validator and class-transformer libraries to perform complex validations and data transformations in a declarative way.


Essential Configuration for Secure Contracts

new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true
})
Enter fullscreen mode Exit fullscreen mode

This configuration represents one of the best practices for ensuring secure input contracts. Let's analyze each property in detail:


Transform: Enabling Automatic Type Transformation

transform: true
Enter fullscreen mode Exit fullscreen mode

What it does:

The transform property enables automatic transformation of input data into instances of the corresponding DTO (Data Transfer Object) classes. Without this option, the data remains as plain JavaScript objects.

Benefits:

  • Type Safety: Data is automatically converted to the correct types defined in the DTO
  • Enhanced Validation: Enables class-instance-based validations
  • Consistency: Ensures data always follows the expected structure

Practical Example:

// DTO
class CreateUserDto {
  @IsString()
  name: string;

  @IsNumber()
  @Min(0)
  age: number;

  @IsEmail()
  email: string;
}

// Controller
@Post('users')
createUser(@Body() createUserDto: CreateUserDto) {
  // With transform: true, createUserDto is an instance of CreateUserDto
  console.log(createUserDto instanceof CreateUserDto); // true

  // Data is automatically converted to the correct types
  console.log(typeof createUserDto.age); // 'number' (even when sending "25" as string)

  return this.userService.create(createUserDto);
}
Enter fullscreen mode Exit fullscreen mode

Whitelist: Filtering Undeclared Properties

whitelist: true
Enter fullscreen mode Exit fullscreen mode

What it does:

The whitelist property automatically removes any property from the input object that is not defined in the DTO through validation decorators.

Benefits:

  • Security: Prevents injection of unexpected properties
  • Data Cleaning: Automatically removes unnecessary data
  • Strict Control: Keeps only explicitly allowed data

Practical Example:

class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

// Request body received:
{
  "name": "John",
  "email": "[email protected]",
  "password": "123456",        // ❌ Will be removed
  "isAdmin": true,             // ❌ Will be removed
  "maliciousScript": "<script>" // ❌ Will be removed
}

// Final object after whitelist:
{
  "name": "John",
  "email": "[email protected]"
  // Only properties with decorators are kept
}
Enter fullscreen mode Exit fullscreen mode

ForbidNonWhitelisted: Rejecting Invalid Data

forbidNonWhitelisted: true
Enter fullscreen mode Exit fullscreen mode

What it does:

This property goes beyond whitelist and generates an HTTP 400 (Bad Request) error when non-allowed properties are sent in the request.

Benefits:

  • Immediate Feedback: Informs the client about invalid data
  • Proactive Security: Blocks attempts to send malicious data
  • Easier Debugging: Helps developers identify contract errors

Practical Example:

class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

// Request with non-allowed property:
{
  "name": "John",
  "email": "[email protected]",
  "unauthorizedField": "value"
}

// Result: HTTP 400 error with message:
{
  "statusCode": 400,
  "message": [
    "property unauthorizedField should not exist"
  ],
  "error": "Bad Request"
}
Enter fullscreen mode Exit fullscreen mode

Complete Implementation: From DTO to Controller

1. Creating a Robust DTO

import { IsString, IsEmail, IsNumber, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @Length(2, 50)
  name: string;

  @IsEmail()
  email: string;

  @IsNumber()
  @Min(18)
  @Max(120)
  age: number;

  @IsOptional()
  @IsString()
  bio?: string;
}
Enter fullscreen mode Exit fullscreen mode

2. Configuring ValidationPipe Globally

// main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({
    transform: true,
    whitelist: true,
    forbidNonWhitelisted: true,
  }));

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

3. Using in Controller

@Controller('users')
export class UsersController {
  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    // Data guaranteed to be valid and typed
    return this.userService.create(createUserDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages of Secure Input Contracts

1. Enhanced Security

  • Prevention of property injection attacks
  • Strict validation of data types
  • Blocking of malicious data

2. Maintainability

  • Clear and well-documented contracts
  • Centralized validations in DTOs
  • Reduction of manual validation code

3. Developer Experience

  • Type safety at compile time
  • Descriptive error messages
  • IDE auto-completion

4. Performance

  • Optimized validation
  • Efficient data transformation
  • Lower manual validation overhead

Advanced Use Cases

Conditional Validation

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  name?: string;

  @IsOptional()
  @IsEmail()
  email?: string;

  @ValidateIf(o => o.email !== undefined)
  @IsString()
  @MinLength(8)
  password?: string;
}
Enter fullscreen mode Exit fullscreen mode

Custom Validation

export class CreateUserDto {
  @IsString()
  @Validate(CustomUsernameValidator)
  username: string;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The ValidationPipe with the transform: true, whitelist: true, and forbidNonWhitelisted: true configurations represents a robust approach to ensuring secure input contracts in NestJS applications. This configuration offers:

  • Maximum security through filtering and rejection of unauthorized data
  • Type safety with automatic type transformation
  • Superior development experience with declarative validations and clear error messages

Implementing these practices from the beginning of the project ensures a solid foundation for secure, maintainable, and reliable APIs. The ValidationPipe is not just a validation tool, but a guardian that protects your application against malformed data and potential security vulnerabilities.

Top comments (0)