Skip to content
NestJS ns pipes 4 min read

DTOs & Data Modeling

A Data Transfer Object (DTO) is a plain class that defines the shape of data crossing a boundary — typically the request body of an HTTP endpoint. In NestJS, DTOs are the contract that the ValidationPipe enforces and that class-validator decorators annotate. Designing them well keeps your controllers honest, your validation declarative, and your API documentation accurate. The challenge is that create, update, and response shapes overlap heavily, and copy-pasting fields between them quickly rots. The @nestjs/mapped-types package solves this by deriving one DTO from another.

Why classes, not interfaces

DTOs in NestJS must be classes, not TypeScript interfaces or types. Interfaces are erased at compile time, so the runtime has nothing to attach validation metadata to. Classes survive into the emitted JavaScript, which lets class-validator and class-transformer read their decorators and lets the ValidationPipe instantiate and check incoming payloads.

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

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

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsInt()
  @Min(0)
  age?: number;
}

The duplication problem

A typical resource needs at least three shapes: a create DTO (everything required), an update DTO (everything optional), and a response DTO (no password, plus server-generated fields like id). Writing each one by hand means three copies of the same field list, three places to update when a field is renamed, and three opportunities to forget a validation rule.

@nestjs/mapped-types provides four utility functions that build new DTO classes from existing ones while preserving the validation and transformation metadata.

npm install @nestjs/mapped-types

If you are using @nestjs/swagger, import the mapped-type helpers from @nestjs/swagger instead. They behave identically but also carry the OpenAPI schema metadata, so your Swagger docs stay in sync. Do not mix the two import sources for the same DTO.

Deriving update DTOs with PartialType

PartialType returns a new class with every property of the base marked optional, while keeping all the original validation decorators. An update is naturally a partial — clients send only the fields they want to change.

// update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Now UpdateUserDto accepts { name?, email?, password?, age? }, and a supplied email is still validated as an email. No decorators were re-declared.

Selecting and excluding fields

PickType and OmitType carve a new DTO out of a base by choosing or dropping fields. Use them when a shape is a strict subset of another.

// login.dto.ts
import { PickType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// Only email + password, validators preserved
export class LoginDto extends PickType(CreateUserDto, ['email', 'password'] as const) {}
// public-user.dto.ts
import { OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// Everything except the password
export class PublicUserDto extends OmitType(CreateUserDto, ['password'] as const) {}

Composing DTOs with IntersectionType

IntersectionType merges two DTOs into one, combining all properties and their validators. It is ideal for mixing a base entity with cross-cutting concerns like pagination.

import { IntersectionType } from '@nestjs/mapped-types';
import { IsInt, Min, IsOptional } from 'class-validator';

export class PaginationDto {
  @IsOptional()
  @IsInt()
  @Min(1)
  page?: number;
}

// CreateUserDto fields + pagination fields
export class CreateUserWithPagingDto extends IntersectionType(CreateUserDto, PaginationDto) {}

The helpers also compose with one another — for example, PartialType(OmitType(CreateUserDto, ['password'])) builds an update DTO that can never touch the password.

Mapped type reference

HelperResultTypical use
PartialType(Base)All fields optionalUpdate / PATCH DTOs
PickType(Base, keys)Only the chosen fieldsLogin, narrow inputs
OmitType(Base, keys)All fields except the chosen onesResponse DTOs, hiding secrets
IntersectionType(A, B)Union of both field setsMixing in pagination/filters

Wiring DTOs into a controller

DTOs are consumed by binding the class as a parameter type. With a global ValidationPipe, NestJS automatically transforms and validates the body against the declared DTO.

// users.controller.ts
import { Controller, Post, Patch, Body, Param } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.users.create(dto);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    return this.users.update(id, dto);
  }
}

A malformed request is rejected before your handler runs:

curl -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "A", "email": "not-an-email", "password": "123"}'

Output:

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

Best Practices

  • Always model DTOs as classes so validation metadata survives at runtime.
  • Define one canonical create DTO per resource and derive everything else with mapped types.
  • Use PartialType for update DTOs so every field becomes optional without re-declaring validators.
  • Reach for OmitType to strip secrets like passwords from response DTOs.
  • Import mapped-type helpers from @nestjs/swagger when generating OpenAPI docs, and never mix import sources for one DTO.
  • Pass key arrays as as const to keep PickType/OmitType type-safe.
  • Keep DTOs free of business logic — they describe data shape and validation only.
Last updated June 14, 2026
Was this helpful?