Introduction
An API should never assume that the data received from a client is correct. A user might submit an invalid email address, send a negative product price, leave a required field empty, or pass an unexpected value in a request parameter. If the application accepts this data without checking it, problems can appear later in database operations or business logic.
Consider a user registration API. The endpoint expects a name, email address, and password. A mobile application might send all three fields correctly, but another client could send an empty name, an invalid email, or a password that does not meet the application's requirements.
Request validation helps catch these problems before the request reaches the service layer.
NestJS provides pipes for validating incoming request data. Existing applications commonly use ValidationPipe with class-validator and class-transformer. NestJS 12 also supports Standard Schema-compatible libraries through StandardSchemaValidationPipe, allowing developers to use tools such as Zod, Valibot, and ArkType.
This article explains how to add request validation to a NestJS 12 application, implement both validation approaches, return useful error responses, and apply validation consistently across API endpoints.
Understanding Request Validation in NestJS
When a client sends an HTTP request, NestJS processes it through several stages before calling the controller method.
A simplified request flow looks like this:
Client Request
|
v
Middleware
|
v
Guards
|
v
Interceptors
|
v
Pipes
|
v
Controller
|
v
Service
|
v
DatabasePipes are responsible for transforming or validating values before they reach the controller.
For example, consider an endpoint that receives a user ID:
@Get(':id')
findOne(@Param('id') id: number) {
return this.usersService.findOne(id);
}HTTP route parameters arrive as strings. A request such as /users/25 initially provides the value "25", not the number 25.
A pipe can validate and convert this value before the controller uses it.
NestJS provides several built-in pipes, including:
ValidationPipefor class-based validation.StandardSchemaValidationPipefor Standard Schema-compatible validation.ParseIntPipefor integer conversion and validation.ParseBoolPipefor boolean conversion and validation.ParseUUIDPipefor UUID validation.
Choosing the right pipe depends on the type of request data and the validation approach used by the application.
Setting Up a NestJS 12 Application
For this example, we will build a user registration endpoint.
The API will accept the following information:
{
"name": "Rahul Sharma",
"email": "[email protected]",
"password": "StrongPass123"
}The application should reject requests when:
- The name is missing or empty.
- The email address is invalid.
- The password is shorter than eight characters.
- The request contains unexpected properties.
We will first implement this using the familiar class-based validation approach and then introduce Standard Schema validation.
For a NestJS application using npm, install the required packages:
npm install class-validator class-transformerThe class-validator package provides validation decorators, while class-transformer helps convert plain request objects into class instances.
Implementing Request Validation With ValidationPipe
The class-based approach is useful when an existing application already uses DTO classes.
Creating a Request DTO
Create a file named create-user.dto.ts.
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength
} from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}Each decorator defines a rule for a particular property.
IsString() ensures that the value is a string. IsNotEmpty() prevents an empty name. IsEmail() checks the email format, and MinLength(8) requires the password to contain at least eight characters.
Keeping these rules inside a DTO makes the request structure easier to understand and reuse.
Registering a Global Validation Pipe
Creating a DTO does not automatically validate every incoming request. The application must register a validation pipe.
Open main.ts and configure the pipe during application startup:
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
})
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();These three options are important.
whitelist: true removes properties that do not have validation decorators in the DTO.
forbidNonWhitelisted: true changes that behavior by rejecting requests containing unexpected properties instead of silently removing them.
transform: true allows NestJS to transform incoming plain objects into their DTO classes and perform supported primitive conversions.
For example, if a client sends an additional isAdmin property, the request will be rejected rather than allowing an unexpected field to pass into the application.
This is particularly useful for preventing clients from submitting fields that should only be controlled by the server.
Creating the User Registration Endpoint
Now that the DTO and global validation pipe are ready, create the controller.
import {
Body,
Controller,
Post
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body() createUserDto: CreateUserDto) {
return {
message: 'User registration request received',
user: {
name: createUserDto.name,
email: createUserDto.email
}
};
}
}The controller receives a validated CreateUserDto object. The password is deliberately excluded from the response.
In a real application, the controller should pass the DTO to a service that handles password hashing, duplicate email checks, and database persistence. Request validation checks the shape and basic rules of the input; it does not replace business validation.
For example, checking whether an email address is already registered belongs in the service or database layer, not in a simple DTO decorator.
Testing the Endpoint
Start the application:
npm run start:devSend a valid request:
POST /users
Content-Type: application/json{
"name": "Rahul Sharma",
"email": "[email protected]",
"password": "StrongPass123"
}The controller should receive the validated data.
Now try an invalid request:
{
"name": "",
"email": "invalid-email",
"password": "123",
"isAdmin": true
}NestJS rejects the request before the controller method executes. The response contains validation errors explaining which fields failed.
This is the main benefit of using a global validation pipe: developers do not need to repeat the same validation logic in every controller method.
Using Standard Schema Validation in NestJS 12
NestJS 12 also supports Standard Schema-compatible validation libraries.
This approach is useful when a project already uses Zod, Valibot, or ArkType to define data structures.
Instead of defining validation rules through decorators, developers define a schema and infer the TypeScript type from it.
For this example, we will use Zod.
Install the package:
npm install zodCreating a Zod Schema
Create a file named create-user.schema.ts.
import { z } from 'zod';
export const createUserSchema = z.object({
name: z.string().trim().min(1, 'Name is required'),
email: z.string().email('Enter a valid email address'),
password: z.string().min(
8,
'Password must contain at least 8 characters'
)
});
export type CreateUserDto =
z.infer<typeof createUserSchema>;The schema describes the expected request structure.
The trim() method removes surrounding whitespace before the name is checked. The min() rule prevents an empty name, while the email and password rules validate their respective fields.
The TypeScript type is generated from the schema:
export type CreateUserDto =
z.infer<typeof createUserSchema>;This avoids maintaining separate validation rules and TypeScript interfaces for the same request.
If the schema changes, the inferred type changes with it.
Registering StandardSchemaValidationPipe
The next step is registering the Standard Schema validation pipe.
In main.ts, add the following configuration:
import { NestFactory } from '@nestjs/core';
import {
StandardSchemaValidationPipe
} from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new StandardSchemaValidationPipe()
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();This pipe validates request parameters that declare a Standard Schema-compatible schema.
Unlike the class-based ValidationPipe, it does not depend on decorator metadata or DTO class instances.
Applying the Schema to a Controller
Update the controller to attach the schema to the request body:
Join the conversation! Your thoughts help the community grow.