NestJS in Production · Structure, Validation and Errors

Validating Input with DTOs and ValidationPipe

Validate and transform request bodies with class-validator DTOs and a global ValidationPipe that strips unknown fields.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A POST /users endpoint with a CreateUserDto validated by a global ValidationPipe: wrong types and missing fields produce a 400 with messages, unknown properties are rejected, and query strings are transformed into numbers.

The idea

  • A DTO class describes the expected input with class-validator decorators (@IsEmail(), @Length(), @IsInt()).
  • ValidationPipe transforms the plain body into a DTO instance and validates it before the handler runs.
  • whitelist: true strips properties without decorators; forbidNonWhitelisted: true rejects them (good for catching client bugs and mass-assignment attempts).
  • transform: true converts primitive types (route and query parameters) to the declared types.

The code

A global ValidationPipe with whitelisting
import "reflect-metadata";
import { Body, Controller, Get, Module, Post, Query, ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { IsEmail, IsInt, IsOptional, Length, Max, Min } from "class-validator";
import { Type } from "class-transformer";

class CreateUserDto {
  @IsEmail() email: string;
  @Length(2, 50) name: string;
}

class ListQuery {
  @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit: number = 20;
}

@Controller("users")
class UsersController {
  @Post() create(@Body() dto: CreateUserDto) { return { created: dto, isDto: dto instanceof CreateUserDto }; }
  @Get() list(@Query() q: ListQuery) { return { limit: q.limit, type: typeof q.limit }; }
}

@Module({ controllers: [UsersController] })
class AppModule {}

async function main() {
  const app = await NestFactory.create(AppModule, { logger: false });
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
  await app.listen(0, "127.0.0.1");
  const base = `http://127.0.0.1:${app.getHttpServer().address().port}`;
  const post = async (body: object) => {
    const r = await fetch(`${base}/users`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
    console.log(r.status, JSON.stringify(await r.json()));
  };
  await post({ email: "[email protected]", name: "Ana" });
  await post({ email: "nope", name: "A" });
  await post({ email: "[email protected]", name: "Ben", role: "admin" });
  for (const q of ["?limit=5", "?limit=500"]) {
    const r = await fetch(`${base}/users${q}`);
    console.log(q, r.status, JSON.stringify(await r.json()));
  }
  await app.close();
}
main();

Output

201 {"created":{"email":"[email protected]","name":"Ana"},"isDto":true}
400 {"message":["email must be an email","name must be longer than or equal to 2 characters"],"error":"Bad Request","statusCode":400}
400 {"message":["property role should not exist"],"error":"Bad Request","statusCode":400}
?limit=5 200 {"limit":5,"type":"number"}
?limit=500 400 {"message":["limit must not be greater than 100"],"error":"Bad Request","statusCode":400}

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Defining DTOs as interfaces or types: validators need classes and decorators at runtime.
  • Forgetting whitelist, so extra fields like role reach your service.
  • Forgetting transform, so query parameters stay strings and limit + 1 becomes "51".
  • Validating nested objects without @ValidateNested() and @Type(() => Child).

Key points

  • DTO classes plus a global ValidationPipe validate every request.
  • whitelist and forbidNonWhitelisted block unexpected fields.
  • transform converts parameters to the declared types.

Further reading: Validation · class-validator

Practice questions

Why must NestJS DTOs be classes rather than interfaces?

Answer

Validation and transformation rely on decorators and runtime type metadata, which only exist on classes; interfaces are erased at compile time.

What do whitelist and forbidNonWhitelisted do?

Answer

whitelist strips properties without validation decorators; forbidNonWhitelisted returns a 400 when such properties are present.

What does transform: true change?

Answer

The pipe returns DTO class instances and converts primitive parameters (route, query) to their declared types, such as strings to numbers.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?