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:
import {
Body,
Controller,
Post
} from '@nestjs/common';
import {
createUserSchema,
CreateUserDto
} from './create-user.schema';
@Controller('users')
export class UsersController {
@Post()
create(
@Body({ schema: createUserSchema })
user: CreateUserDto
) {
return {
message: 'User registration request received',
user: {
name: user.name,
email: user.email
}
};
}
}The schema option connects the request body to the Zod validation rules.
The global StandardSchemaValidationPipe reads that metadata and validates the incoming value before the controller executes.
The controller receives the inferred CreateUserDto type, so developers get TypeScript support without creating a separate DTO class.
Choosing Between ValidationPipe and StandardSchemaValidationPipe
Both approaches are supported. The right choice depends on the existing project structure and how the team prefers to define validation rules.
| Comparison | ValidationPipe | StandardSchemaValidationPipe |
|---|---|---|
| Validation rules | Class decorators | Schema definitions |
| Common libraries | class-validator | Zod, Valibot, ArkType |
| Type definition | DTO class | Inferred from schema |
| Reflection metadata | Used for class-based validation | Not required |
| Existing NestJS projects | Suitable for decorator-based DTOs | Suitable for schema-based validation |
| Reusable validation rules | DTO classes and decorators | Composable schemas |
You do not need to migrate all existing DTOs to Zod just because you are using NestJS 12.
Both pipes can be registered in the same application. This makes gradual adoption possible. For example, an existing application can continue using ValidationPipe for older modules while introducing StandardSchemaValidationPipe in new modules.
Avoid maintaining two different definitions of the same validation rules for a single endpoint. That creates unnecessary duplication and makes future changes harder to manage.
Returning Consistent Validation Errors
Validation errors should help API consumers understand what went wrong without exposing internal application details.
For example, an invalid registration request might produce multiple errors. Returning a clear error structure makes it easier for frontend developers to display field-level messages.
With Standard Schema validation, you can customize the exception response using exceptionFactory.
import {
StandardSchemaValidationPipe,
UnprocessableEntityException
} from '@nestjs/common';
app.useGlobalPipes(
new StandardSchemaValidationPipe({
exceptionFactory: (issues) =>
new UnprocessableEntityException({
message: 'Validation failed',
errors: issues.map((issue) => ({
path: issue.path
?.map((segment) =>
typeof segment === 'object'
? segment.key
: segment
)
.join('.'),
message: issue.message
}))
})
})
);This example returns an HTTP 422 response when validation fails.
A response can look like this:
{
"message": "Validation failed",
"errors": [
{
"path": "email",
"message": "Invalid email address"
},
{
"path": "password",
"message": "Password must contain at least 8 characters"
}
]
}The path identifies the field that failed validation, while message explains the problem.
This structure is useful for applications where frontend forms need to display validation messages next to individual input fields.
For consistency, use a similar error response structure across the application, including errors produced by business validation.
Validating Route Parameters and Query Parameters
Request validation is not limited to JSON request bodies. Route parameters and query parameters also come from external clients and should be checked before they are used.
Consider an endpoint that retrieves a user by ID:
import {
Controller,
Get,
Param,
ParseIntPipe
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number
) {
return {
message: 'User retrieved successfully',
userId: id
};
}
}The ParseIntPipe converts the route parameter into an integer. If someone sends /users/abc, NestJS rejects the request instead of passing an invalid value to the controller.
For schema-based validation, the same approach can be used with a Standard Schema:
import { z } from 'zod';
const userIdSchema = z.coerce
.number()
.int()
.positive();
@Get(':id')
findOne(
@Param('id', { schema: userIdSchema })
id: number
) {
return this.usersService.findOne(id);
}The coerce method converts the incoming string into a number before applying the remaining validation rules.
This is useful when validating query filters, pagination values, and resource identifiers.
Advantages and Disadvantages of Request Validation
| Advantages | Disadvantages |
|---|---|
| Rejects invalid data before business logic executes | Adds validation configuration |
| Reduces repetitive checks in controllers | Incorrect rules can reject valid requests |
| Provides consistent API error responses | Complex validation may require custom logic |
| Improves type safety | Runtime validation does not replace TypeScript checks |
| Supports reusable DTOs and schemas | Maintaining duplicate validation rules creates complexity |
| Helps protect application data | Does not replace authorization or database constraints |
Request validation should be treated as one layer of application protection. It cannot determine whether a user has permission to access a resource or whether a database operation is valid in the current business context.
Common Mistakes to Avoid
Creating DTOs Without Registering a Validation Pipe
Adding decorators to a DTO does not automatically activate validation. Register ValidationPipe globally or at the required controller or route level.
Forgetting Validation Decorators
When whitelist is enabled, properties without validation decorators are removed from the resulting DTO.
Make sure every accepted property has the appropriate validation rules.
Using TypeScript Types as Runtime Validation
A TypeScript interface disappears after compilation. It cannot validate incoming HTTP data.
Use a DTO class with validation decorators or a runtime schema such as Zod.
Returning Sensitive Information
Never include passwords, authentication tokens, or internal database details in validation responses.
Only return information that helps the API consumer correct the request.
Repeating Validation Rules in Multiple Places
Maintaining the same rules in a DTO, controller, and service creates unnecessary duplication.
Keep request validation in DTOs or schemas and reserve service-level checks for business rules.
Best Practices for Production Applications
Request validation should be consistent across all API endpoints. The following practices help maintain a reliable validation layer.
- Use global validation pipes when most endpoints share the same validation requirements.
- Keep request DTOs and schemas separate from controller implementations.
- Enable
whitelistandforbidNonWhitelistedwhen unexpected request properties should be rejected. - Use transformation carefully, especially when converting strings into numbers, dates, or other data types.
- Keep validation error responses consistent so frontend applications can handle them easily.
- Validate nested objects and arrays rather than checking only top-level properties.
- Avoid placing database queries inside basic DTO validation rules. Check duplicate records and business constraints in the service layer.
- Never assume that a validated request is an authorized request. Use guards and service-level authorization checks where required.
- Test both valid and invalid inputs, including missing fields, incorrect types, empty values, and unexpected properties.
- Keep validation dependencies updated and review their behavior when upgrading the application.
For important business operations, also enforce data integrity at the database level. For example, validating an email address does not prevent two concurrent requests from registering the same email. A unique database constraint is still necessary.
Troubleshooting Common Validation Problems
| Problem | Possible cause | Solution |
|---|---|---|
| DTO validation does not run | ValidationPipe is not registered | Register the pipe globally or locally |
| Extra properties are accepted | Whitelist options are missing | Configure whitelist and forbidNonWhitelisted |
| Numeric parameters arrive as strings | HTTP parameters are strings by default | Use ParseIntPipe or schema coercion |
| Zod schema is not applied | StandardSchemaValidationPipe is missing | Register the pipe and attach the schema to the parameter |
| TypeScript reports missing property errors | DTO and schema types are inconsistent | Use the inferred schema type |
| Validation errors are difficult to display | Default error response does not match frontend requirements | Configure an exception factory |
| Valid nested data fails validation | Nested validation rules are incomplete | Define nested DTO or schema validation rules |
When troubleshooting, test the endpoint with both valid and invalid payloads. Checking only the successful response will not confirm that the validation rules are working correctly.
Summary
Request validation is an important part of building reliable APIs with NestJS. It ensures that incoming data follows the expected structure before it reaches controllers and business logic.
NestJS 12 supports both the traditional ValidationPipe approach with class-validator and class-transformer and the newer StandardSchemaValidationPipe approach with libraries such as Zod, Valibot, and ArkType.
For existing applications, there is no need to replace working DTO-based validation. Teams can continue using their current implementation or introduce schema-based validation gradually.
The important part is to define clear validation rules, reject unexpected input, return useful error messages, and test the behavior of every important endpoint.
A well-configured validation layer makes APIs easier to maintain and helps prevent invalid data from reaching the parts of the application where it can cause bigger problems.

Join the conversation! Your thoughts help the community grow.