Пошук уроків, статей та іншого контенту
Поєднаєте перетворення типів із перевіркою даних для коректної обробки чисел, дат, вкладених об’єктів і масивів.
HTTP-запити передають дані у вигляді простих значень:
параметри URL і query-параметри зазвичай є рядками;
JSON містить числа, рядки, логічні значення, масиви та об’єкти, але не екземпляри TypeScript-класів;
дати в JSON передаються як рядки.
Наприклад, значення ?page=2 надходить у застосунок як рядок "2", а не як число 2.
У NestJS за перетворення і перевірку DTO зазвичай відповідають:
ValidationPipe — запускає перевірку за допомогою class-validator;
class-transformer — перетворює звичайні об’єкти та рядки на типи, описані в DTO;
@Type() — явно вказує, у який тип потрібно перетворити значення;
@ValidateNested() — запускає перевірку вкладеного DTO.
Увімкніть глобальний ValidationPipe у main.ts:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
await app.listen(3000);
}
bootstrap();transform: trueЦя опція вмикає перетворення вхідних даних у тип DTO.
Без неї параметр page у такому методі залишатиметься рядком:
@Get()
findAll(@Query() query: ListOrdersQuery) {
// query.page може бути рядком "2"
}З transform: true та правильною декларацією DTO значення стане числом:
query.page; // 2transform: true також перетворює plain object на екземпляр класу DTO. Це важливо для вкладеної валідації.
whitelist: trueВидаляє властивості, для яких у DTO немає валідаційних декораторів.
Наприклад, якщо DTO описує лише name, додаткове поле role буде вилучено.
forbidNonWhitelisted: trueЗамість тихого видалення невідомих полів повертає помилку клієнту. Це корисно, коли зайві поля можуть свідчити про помилку в клієнтському коді або небажану спробу змінити дані.
Query-параметри та параметри маршруту надходять як рядки. Для явного перетворення використовуйте @Type(() => Number).
import { Type } from 'class-transformer';
import { IsInt, IsOptional, Max, Min } from 'class-validator';
export class ListOrdersQuery {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit = 20;
}Тепер запит:
GET /orders?page=2&limit=10перетворюється приблизно на такі значення:
{
page: 2,
limit: 10,
}Але запит:
GET /orders?page=abcне пройде валідацію, тому що значення не можна коректно використати як ціле число.
Порядок декораторів у DTO не є способом керування порядком виконання. Важливо, щоб були присутні обидві частини:
@Type(() => Number) — перетворення;
@IsInt(), @Min(), @Max() — перевірка.
Дата в JSON передається рядком:
{
"deliveryDate": "2026-09-15T10:30:00.000Z"
}Щоб отримати об’єкт Date, використайте @Type(() => Date), а для перевірки — @IsDate():
import { Type } from 'class-transformer';
import { IsDate, IsNotEmpty } from 'class-validator';
export class DeliveryDto {
@IsNotEmpty()
@Type(() => Date)
@IsDate()
deliveryDate: Date;
}Після трансформації:
deliveryDate instanceof Date; // trueРядок із некоректною датою не пройде @IsDate().
Варто пам’ятати, що формат дати також має значення для часового поясу. Для API краще використовувати однозначний ISO-формат, наприклад:
2026-09-15T10:30:00.000ZДля вкладеного DTO потрібні два декоратори:
@ValidateNested() — перевіряє властивості вкладеного об’єкта;
@Type(() => NestedDto) — перетворює plain object на екземпляр вкладеного DTO.
Розглянемо DTO адреси:
import { IsNotEmpty, IsPostalCode, IsString, Length } from 'class-validator';
export class AddressDto {
@IsString()
@IsNotEmpty()
@Length(2, 100)
city: string;
@IsString()
@IsNotEmpty()
@Length(5, 200)
street: string;
@IsPostalCode('any')
postalCode: string;
}Тепер використаємо його в іншому DTO:
import { Type } from 'class-transformer';
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { AddressDto } from './address.dto';
export class CustomerDto {
@IsNotEmpty()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}Без @Type(() => AddressDto) NestJS не матиме достатньо інформації, щоб перетворити address на екземпляр AddressDto. У результаті вкладена валідація може не спрацювати так, як очікується.
Приклад коректного JSON:
{
"name": "Олена",
"address": {
"city": "Львів",
"street": "вул. Шевченка, 10",
"postalCode": "79000"
}
}Для масиву DTO потрібно вказати, що:
властивість є масивом;
кожен елемент потрібно перевірити;
кожен елемент потрібно перетворити на потрібний клас.
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsInt,
IsPositive,
ValidateNested,
} from 'class-validator';
export class OrderItemDto {
@Type(() => Number)
@IsInt()
@IsPositive()
productId: number;
@Type(() => Number)
@IsInt()
@IsPositive()
quantity: number;
}
export class CreateOrderDto {
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}У цьому прикладі:
@IsArray() перевіряє, що items є масивом;
@ArrayMinSize(1) вимагає хоча б один елемент;
@ValidateNested({ each: true }) перевіряє кожен елемент;
@Type(() => OrderItemDto) перетворює елементи на OrderItemDto.
Коректне тіло запиту:
{
"items": [
{
"productId": 10,
"quantity": 2
},
{
"productId": 20,
"quantity": 1
}
]
}Для масиву чисел або рядків також потрібно окремо вказати перевірку кожного елемента.
import { Type } from 'class-transformer';
import { IsArray, IsInt, ArrayMinSize } from 'class-validator';
export class ProductIdsDto {
@IsArray()
@ArrayMinSize(1)
@Type(() => Number)
@IsInt({ each: true })
productIds: number[];
}Опція { each: true } означає, що декоратор застосовується до кожного елемента масиву, а не до масиву як одного значення.
Без неї @IsInt() перевіряв би саму властивість productIds, хоча властивість є масивом.
Нижче наведено приклад контролера, який приймає замовлення з числовими значеннями, датою, вкладеним об’єктом і масивом вкладених об’єктів.
// src/orders/dto/address.dto.ts
import { IsNotEmpty, IsPostalCode, IsString, Length } from 'class-validator';
export class AddressDto {
@IsString()
@IsNotEmpty()
@Length(2, 100)
city: string;
@IsString()
@IsNotEmpty()
@Length(5, 200)
street: string;
@IsPostalCode('any')
postalCode: string;
}// src/orders/dto/order-item.dto.ts
import { Type } from 'class-transformer';
import { IsInt, IsPositive } from 'class-validator';
export class OrderItemDto {
@Type(() => Number)
@IsInt()
@IsPositive()
productId: number;
@Type(() => Number)
@IsInt()
@IsPositive()
quantity: number;
}// src/orders/dto/create-order.dto.ts
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsDate,
IsInt,
IsNotEmpty,
IsPositive,
ValidateNested,
} from 'class-validator';
import { AddressDto } from './address.dto';
import { OrderItemDto } from './order-item.dto';
export class CreateOrderDto {
@Type(() => Number)
@IsInt()
@IsPositive()
customerId: number;
@Type(() => Date)
@IsDate()
deliveryDate: Date;
@IsNotEmpty()
@ValidateNested()
@Type(() => AddressDto)
deliveryAddress: AddressDto;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
}// src/orders/orders.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateOrderDto } from './dto/create-order.dto';
@Controller('orders')
export class OrdersController {
@Post()
create(@Body() dto: CreateOrderDto) {
return {
customerId: dto.customerId,
deliveryDate: dto.deliveryDate.toISOString(),
deliveryDateIsDate: dto.deliveryDate instanceof Date,
city: dto.deliveryAddress.city,
items: dto.items.map((item) => ({
productId: item.productId,
quantity: item.quantity,
productIdIsNumber: typeof item.productId === 'number',
quantityIsNumber: typeof item.quantity === 'number',
})),
};
}
}// src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
@Module({
controllers: [OrdersController],
})
export class OrdersModule {}// src/app.module.ts
import { Module } from '@nestjs/common';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [OrdersModule],
})
export class AppModule {}// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
await app.listen(3000);
}
bootstrap();Для такого контролера можна надіслати запит:
POST /orders
Content-Type: application/json{
"customerId": 42,
"deliveryDate": "2026-09-15T10:30:00.000Z",
"deliveryAddress": {
"city": "Львів",
"street": "вул. Шевченка, 10",
"postalCode": "79000"
},
"items": [
{
"productId": 100,
"quantity": 2
}
]
}У методі create:
dto.customerId є числом;
dto.deliveryDate є об’єктом Date;
dto.deliveryAddress перевіряється як AddressDto;
кожен елемент dto.items перевіряється як OrderItemDto.
class-transformer підтримує глобальне неявне перетворення:
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
});У такому режимі бібліотека намагається визначати типи за метаданими TypeScript. Проте для API краще явно використовувати @Type(() => Number), @Type(() => Date) та інші декоратори.
Явний запис:
показує намір безпосередньо в DTO;
легше читається;
менше залежить від метаданих і налаштувань;
особливо важливий для масивів і вкладених об’єктів.
Коли використовується ValidationPipe({ transform: true }), обробка DTO концептуально виглядає так:
NestJS отримує дані HTTP-запиту.
class-transformer створює екземпляр DTO і перетворює значення за @Type().
class-validator перевіряє отриманий екземпляр.
Якщо є помилки, NestJS повертає відповідь із помилкою валідації.
Якщо помилок немає, контролер отримує вже перетворений DTO.
Тому бізнес-логіка контролера може працювати з числами, датами та вкладеними класами, а не повторювати ручне перетворення рядків.
new ValidationPipe()У такому випадку перевірка працює, але значення query-параметрів і параметрів маршруту залишаються рядками.
Використовуйте:
new ValidationPipe({
transform: true,
})@Type() для числа@IsInt()
page: number;Анотація TypeScript number не перетворює значення під час виконання. Для рядка "2" потрібен явний декоратор:
@Type(() => Number)
@IsInt()
page: number;@Type() для вкладеного DTO@ValidateNested()
address: AddressDto;Додайте:
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;@IsArray()
items: OrderItemDto[];Це перевіряє лише те, що items є масивом. Для перевірки кожного елемента потрібні:
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];@ValidateNested() без each: trueДля одного вкладеного об’єкта достатньо:
@ValidateNested()
address: AddressDto;Для масиву вкладених об’єктів потрібно:
@ValidateNested({ each: true })
items: OrderItemDto[];whitelist перевіряє всі властивостіwhitelist працює лише з властивостями, на яких є декоратори валідації. Якщо властивість має бути дозволена, але не має окремого обмеження, додайте відповідний декоратор, наприклад @IsString() або @IsOptional().
ValidationPipe перевіряє DTO, а class-transformer перетворює вхідні значення.
Для роботи перетворення потрібно встановити transform: true.
Для чисел використовуйте @Type(() => Number).
Для дат використовуйте @Type(() => Date) разом із @IsDate().
Для вкладеного DTO потрібні @ValidateNested() і @Type().
Для масиву вкладених об’єктів використовуйте @ValidateNested({ each: true }).
Для масивів простих значень використовуйте декоратори з { each: true }.
whitelist видаляє невідомі поля, а forbidNonWhitelisted перетворює їх на помилку.
Явне перетворення через @Type() робить DTO передбачуванішим і зрозумілішим.