Custom DTOs
While NestJS CRUD Automator automatically generates DTOs, you can override them with custom DTOs when you need more control over the data structure, validation, or transformation.
When to Use Custom DTOs
Use custom DTOs when you need:
- Complex validation logic not covered by automatic generation
- Different property names between API and database
- Computed or derived properties
- Specific Swagger documentation
- Integration with existing DTO classes
Replacing Auto-Generated DTOs
Replace auto-generated DTOs with custom ones:
user.controller.ts
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { UserResponseDto } from "./dto/user-response.dto";
@ApiController<UserEntity>({
entity: UserEntity,
routes: {
[EApiRouteType.CREATE]: {
dto: {
body: CreateUserDto,
response: UserResponseDto,
},
},
[EApiRouteType.UPDATE]: {
dto: {
body: UpdateUserDto,
response: UserResponseDto,
},
},
[EApiRouteType.GET]: {
dto: {
response: UserResponseDto,
},
},
},
})
Creating Custom Request DTOs
Create DTO
dto/create-user.dto.ts
import { ApiProperty } from "@nestjs/swagger";
import {
IsString,
IsEmail,
MinLength,
MaxLength,
Matches,
IsOptional,
IsEnum,
} from "class-validator";
export enum UserRole {
USER = "user",
ADMIN = "admin",
MODERATOR = "moderator",
}
export class CreateUserDto {
@ApiProperty({
description: "Username",
example: "john_doe",
minLength: 3,
maxLength: 20,
})
@IsString()
@MinLength(3)
@MaxLength(20)
@Matches(/^[a-zA-Z0-9_-]+$/, {
message: "Username can only contain letters, numbers, underscores, and hyphens",
})
username: string;
@ApiProperty({
description: "Email address",
example: "john@example.com",
})
@IsEmail()
email: string;
@ApiProperty({
description: "Password",
example: "SecurePass123!",
minLength: 8,
})
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: "Password must contain uppercase, lowercase, number, and special character",
})
password: string;
@ApiProperty({
description: "User role",
enum: UserRole,
example: UserRole.USER,
required: false,
})
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
Update DTO
dto/update-user.dto.ts
import { PartialType, OmitType } from "@nestjs/swagger";
import { CreateUserDto } from "./create-user.dto";
// Make all fields optional and exclude password
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ["password"] as const)
) {}
Creating Custom Response DTOs
dto/user-response.dto.ts
import { ApiProperty } from "@nestjs/swagger";
import { Exclude, Expose, Transform } from "class-transformer";
export class UserResponseDto {
@ApiProperty({
description: "User ID",
example: "550e8400-e29b-41d4-a716-446655440000",
})
@Expose()
id: string;
@ApiProperty({
description: "Username",
example: "john_doe",
})
@Expose()
username: string;
@ApiProperty({
description: "Email address",
example: "john@example.com",
})
@Expose()
email: string;
@ApiProperty({
description: "User role",
example: "user",
})
@Expose()
role: string;
@ApiProperty({
description: "Full name",
example: "John Doe",
})
@Expose()
@Transform(({ obj }) => `${obj.firstName} ${obj.lastName}`)
fullName: string;
@ApiProperty({
description: "Account creation date",
example: "2024-01-01T00:00:00.000Z",
})
@Expose()
createdAt: Date;
// Exclude sensitive fields
@Exclude()
password: string;
@Exclude()
deletedAt: Date;
}
Nested DTOs
Create DTOs with nested objects:
dto/create-post.dto.ts
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsArray, ValidateNested, IsOptional } from "class-validator";
import { Type } from "class-transformer";
export class PostMetadataDto {
@ApiProperty({
description: "Featured image URL",
required: false,
})
@IsOptional()
@IsString()
featuredImage?: string;
@ApiProperty({
description: "SEO keywords",
type: [String],
required: false,
})
@IsOptional()
@IsArray()
@IsString({ each: true })
keywords?: string[];
}
export class CreatePostDto {
@ApiProperty({
description: "Post title",
example: "My First Post",
})
@IsString()
title: string;
@ApiProperty({
description: "Post content",
example: "This is the content of my post",
})
@IsString()
content: string;
@ApiProperty({
description: "Post metadata",
type: PostMetadataDto,
required: false,
})
@IsOptional()
@ValidateNested()
@Type(() => PostMetadataDto)
metadata?: PostMetadataDto;
}
Discriminated Union DTOs
Create polymorphic DTOs:
dto/create-payment.dto.ts
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, ValidateIf } from "class-validator";
export class CreatePaymentDto {
@ApiProperty({
description: "Payment type",
enum: ["card", "paypal"],
example: "card",
})
@IsString()
@IsNotEmpty()
type: "card" | "paypal";
// Card-specific fields
@ApiProperty({
description: "Card number",
example: "4242424242424242",
required: false,
})
@ValidateIf(o => o.type === "card")
@IsNotEmpty()
cardNumber?: string;
@ApiProperty({
description: "CVV",
example: "123",
required: false,
})
@ValidateIf(o => o.type === "card")
@IsNotEmpty()
cvv?: string;
// PayPal-specific fields
@ApiProperty({
description: "PayPal email",
example: "user@paypal.com",
required: false,
})
@ValidateIf(o => o.type === "paypal")
@IsNotEmpty()
paypalEmail?: string;
}
Extending Auto-Generated DTOs
Extend auto-generated DTOs with additional logic:
dto/create-user-extended.dto.ts
import { CreateUserDto } from "./generated/create-user.dto"; // Auto-generated
import { IsOptional, IsUrl } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class CreateUserExtendedDto extends CreateUserDto {
@ApiProperty({
description: "Profile picture URL",
example: "https://example.com/avatar.jpg",
required: false,
})
@IsOptional()
@IsUrl()
avatarUrl?: string;
}
Partial DTOs
Create partial DTOs for patch updates:
dto/patch-user.dto.ts
import { PartialType } from "@nestjs/swagger";
import { UpdateUserDto } from "./update-user.dto";
export class PatchUserDto extends PartialType(UpdateUserDto) {}
Pick and Omit
Use utility types to create DTOs from existing ones:
dto/user-public.dto.ts
import { PickType } from "@nestjs/swagger";
import { UserResponseDto } from "./user-response.dto";
// Only include public fields
export class UserPublicDto extends PickType(UserResponseDto, [
"id",
"username",
"createdAt",
] as const) {}
dto/user-safe.dto.ts
import { OmitType } from "@nestjs/swagger";
import { UserResponseDto } from "./user-response.dto";
// Exclude sensitive fields
export class UserSafeDto extends OmitType(UserResponseDto, [
"email",
"role",
] as const) {}
Intersection Types
Combine multiple DTOs:
dto/create-post-with-author.dto.ts
import { IntersectionType } from "@nestjs/swagger";
import { CreatePostDto } from "./create-post.dto";
import { AuthorInfoDto } from "./author-info.dto";
export class CreatePostWithAuthorDto extends IntersectionType(
CreatePostDto,
AuthorInfoDto
) {}
DTO Transformation
Transform DTO before processing:
dto/create-user.dto.ts
import { Transform } from "class-transformer";
export class CreateUserDto {
@Transform(({ value }) => value.toLowerCase().trim())
email: string;
@Transform(({ value }) => value.trim())
username: string;
@Transform(({ value }) => value.split(",").map((tag: string) => tag.trim()))
tags: string[];
}
Validation Groups
Use validation groups for different scenarios:
dto/update-user.dto.ts
import { IsNotEmpty, IsEmail } from "class-validator";
export class UpdateUserDto {
@IsEmail({}, { groups: ["user", "admin"] })
email?: string;
@IsNotEmpty({ groups: ["admin"] })
role?: string;
}
Use in controller:
import { ValidationPipe } from "@nestjs/common";
app.useGlobalPipes(
new ValidationPipe({
groups: ["user"], // Apply user validation group
})
);
Next Steps
- Validation - Advanced validation patterns
- Transformers - Transform DTO data
- Core Concepts - DTOs - Automatic DTO generation
Last updated on