Back to Blog
TypeScriptNestJSValidation

TypeScript Runtime Validation: A Practical Guide to class-validator

TypeScript types only exist at compile time. class-validator decorators fill the runtime gap — one DTO handles type constraints, input validation, and API documentation all at once.

Author: ekent·Published on March 1, 2026

TypeScript delivers a fantastic developer experience: IDE hints, type inference, and compile-time errors. But there's a blind spot that's easy to overlook: TypeScript types don't exist at runtime.

You declare name: string, but if a client sends name: 123, TypeScript won't stop it. Your code will silently treat a number as a string until something breaks downstream.

That's the problem class-validator solves: actually verifying the shape and content of data when your application is running.

Two Layers of Protection: Compile-Time vs Runtime

Client request ──► [Runtime validation] ──► [TypeScript type system] ──► Business logic
                   class-validator           Compile-time type checking
                   (blocks invalid data)     (IDE hints + type inference)

These two layers complement rather than replace each other. TypeScript guarantees type safety within your own code; class-validator guarantees that external input matches expectations.

Core Concept: DTOs

A DTO (Data Transfer Object) is a class that describes exactly what data an endpoint accepts or returns. In NestJS, DTOs are the natural home for class-validator decorators:

// create-user.dto.ts
import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @IsEmail()
  email: string;

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

  @IsOptional()
  @IsString()
  department?: string;
}

Decorators are documentation: reading this DTO tells you everything about the endpoint's contract — no separate comments needed.

Enabling the Validation Pipe in NestJS

NestJS connects class-validator to the request lifecycle via ValidationPipe. We recommend enabling it globally:

// main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,             // strip undeclared fields automatically
    forbidNonWhitelisted: true,  // throw an error if unknown fields are present
    transform: true,             // auto-cast raw input to DTO class instances
  }),
);

Three options worth understanding:

  • whitelist: true: extra fields sent by the client are silently stripped, preventing accidental database writes
  • forbidNonWhitelisted: true: if you want to know when clients are probing your API with unexpected fields, enable this
  • transform: true: URL parameters arrive as strings; this option auto-converts them to the types declared in the DTO

Quick Reference: Common Decorators

// Strings
@IsString()
@MinLength(2)
@MaxLength(100)
@Matches(/^[a-z]+$/)

// Numbers
@IsInt()
@IsNumber()
@Min(0)
@Max(100)

// Boolean / Enum
@IsBoolean()
@IsEnum(UserRole)

// Formats
@IsEmail()
@IsUrl()
@IsDate()
@IsISO8601()

// Presence
@IsOptional()    // field may be absent (skips other validators if missing)
@IsNotEmpty()    // cannot be empty string / null / undefined

// Arrays
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(10)

Validating Nested Objects

When a DTO contains nested objects, combine @ValidateNested() with @Type():

import { Type } from 'class-transformer';

export class AddressDto {
  @IsString()
  city: string;

  @IsString()
  street: string;
}

export class CreateCompanyDto {
  @IsString()
  name: string;

  @ValidateNested()
  @Type(() => AddressDto)  // class-transformer instantiates the nested class
  address: AddressDto;
}

@Type() comes from class-transformer. It tells the pipe to instantiate the raw JSON object as the correct class before class-validator tries to inspect it.

Custom Validators

When built-in decorators aren't enough, you can write your own:

import { registerDecorator, ValidationOptions } from 'class-validator';

export function IsChinesePhone(validationOptions?: ValidationOptions) {
  return function (object: object, propertyName: string) {
    registerDecorator({
      name: 'isChinesePhone',
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: {
        validate(value: any) {
          return /^1[3-9]\d{9}$/.test(value);
        },
        defaultMessage() {
          return 'Please enter a valid Chinese mobile number';
        },
      },
    });
  };
}

// Usage
export class ContactDto {
  @IsChinesePhone()
  phone: string;
}

Custom validators also support async logic — for example, checking whether a value already exists in the database — by returning Promise<boolean> from validate.

Error Response Format

When validation fails, NestJS returns a structured error response out of the box:

{
  "statusCode": 400,
  "message": [
    "name must be longer than or equal to 2 characters",
    "email must be an email"
  ],
  "error": "Bad Request"
}

The format can be customized via exceptionFactory — for instance, grouping errors by field name for easier frontend handling.

Separating Create and Update DTOs

Create and update operations often have different field requirements: all fields are required on create, but mostly optional on update. Use inheritance with PartialType to avoid duplication:

import { PartialType } from '@nestjs/mapped-types';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

// All fields in UpdateUserDto become optional automatically
export class UpdateUserDto extends PartialType(CreateUserDto) {}

PartialType is a NestJS utility that wraps every field in the parent class with @IsOptional() while preserving the original validation rules. One source of truth, two behaviors.

Summary

class-validator extends TypeScript's type safety to runtime, closing the gap between your type declarations and the real world:

ScenarioApproach
Basic field validationBuilt-in decorators (@IsString, @IsEmail, etc.)
Nested objects@ValidateNested() + @Type()
Custom business rulesCustom validator functions
Reuse create/update logicPartialType inheritance
Enable globallyValidationPipe with transform: true

In our projects, we adopted the rule that all external input must pass through a DTO before reaching business logic. Debugging improved noticeably — invalid data is caught at the entry point rather than surfacing as a mysterious error deep in the call stack.


Author: ekent · ek Studio