Пошук уроків, статей та іншого контенту
Застосуєте інтерфейси, класи, модифікатори властивостей і типи TypeScript для безпечного опису вхідних даних.
DTO (Data Transfer Object) — це об’єкт, який описує дані, що передаються між частинами застосунку. У NestJS DTO найчастіше використовують для опису:
тіла HTTP-запиту;
параметрів запиту;
даних, які повертає сервіс;
об’єктів, що передаються між шарами застосунку.
TypeScript допомагає перевірити DTO під час компіляції. Наприклад, він повідомить про помилку, якщо передати число замість рядка.
Однак типи TypeScript не існують під час виконання JavaScript. Тому TypeScript не перевіряє дані, які реально надійшли від клієнта через HTTP. Для перевірки вхідних даних у NestJS зазвичай використовують класи DTO разом із декораторами class-validator.
Інтерфейс описує структуру об’єкта:
interface CreateUserData {
name: string;
email: string;
age: number;
}
const userData: CreateUserData = {
name: 'Олена',
email: 'olena@example.com',
age: 28,
};TypeScript перевірить:
наявність обов’язкових властивостей;
тип кожної властивості;
заборону несумісних значень.
interface CreateUserData {
name: string;
email: string;
age: number;
}
// Помилка: age має бути number
const userData: CreateUserData = {
name: 'Олена',
email: 'olena@example.com',
age: '28',
};Інтерфейси зручні для внутрішніх контрактів між сервісами або функціями. Наприклад, сервіс може приймати об’єкт певної форми:
interface CreateUserData {
name: string;
email: string;
}
function createUser(data: CreateUserData): string {
return `Користувача ${data.name} створено`;
}
createUser({
name: 'Олена',
email: 'olena@example.com',
});Для HTTP-вхідних даних у NestJS краще використовувати класи, а не лише інтерфейси.
interface CreateUserDto {
name: string;
email: string;
}Такий інтерфейс допомагає TypeScript, але після компіляції зникає. NestJS не може використовувати його як runtime-метадані для валідації.
Клас зберігається під час виконання:
export class CreateUserDto {
name: string;
email: string;
}Клас можна використовувати як тип і як значення:
function createUser(dto: CreateUserDto): void {
console.log(dto.name);
}
const dto = new CreateUserDto();
createUser(dto);Для класу DTO можна додати декоратори валідації:
import { IsEmail, IsString, Length } from 'class-validator';
export class CreateUserDto {
@IsString()
@Length(2, 50)
name: string;
@IsEmail()
email: string;
}У цьому прикладі:
name має бути рядком довжиною від 2 до 50 символів;
email має мати коректний формат електронної адреси.
Щоб NestJS виконував таку валідацію для HTTP-запитів, у застосунку потрібно увімкнути ValidationPipe:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(3000);
}
bootstrap();Опції означають:
whitelist: true — залишати лише властивості, описані в DTO;
forbidNonWhitelisted: true — повертати помилку, якщо клієнт передав зайву властивість;
transform: true — дозволити NestJS трансформувати вхідні значення відповідно до типів DTO в підтримуваних випадках.
Властивість без спеціального модифікатора є обов’язковою:
class CreateProductDto {
title: string;
price: number;
}Об’єкт без price не відповідає цьому типу:
const product: CreateProductDto = {
title: 'Клавіатура',
// Помилка: відсутня обов’язкова властивість price
};Знак ? робить властивість необов’язковою:
class UpdateProductDto {
title?: string;
price?: number;
}Такий DTO підходить для часткового оновлення:
const update: UpdateProductDto = {
price: 2500,
};Необов’язкова властивість може мати значення undefined, тому під час використання це потрібно враховувати:
function printTitle(dto: UpdateProductDto): string {
return dto.title ?? 'Назву не вказано';
}У NestJS для необов’язкової властивості також потрібно використовувати декоратор @IsOptional():
import { IsEmail, IsOptional, IsString, Length } from 'class-validator';
export class UpdateUserDto {
@IsOptional()
@IsString()
@Length(2, 50)
name?: string;
@IsOptional()
@IsEmail()
email?: string;
}@IsOptional() повідомляє валідатору, що властивість може бути відсутньою. Якщо властивість присутня, інші декоратори все одно перевірять її значення.
Модифікатор readonly забороняє змінювати властивість після створення об’єкта в TypeScript:
interface User {
readonly id: number;
name: string;
}
const user: User = {
id: 1,
name: 'Олена',
};
// Помилка TypeScript: id доступний лише для читання
user.id = 2;
user.name = 'Марія';readonly захищає від випадкової зміни під час розробки, але не є runtime-захистом. Клієнт все одно може надіслати поле id, тому для вхідного DTO потрібно налаштувати валідацію та фільтрацію властивостей.
Зазвичай ідентифікатор створюється на сервері, тому його не додають до DTO створення:
import { IsEmail, IsString } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}Для властивостей DTO найчастіше використовують:
class CreateTaskDto {
title: string;
completed: boolean;
priority: number;
}Типи визначають, які значення очікує застосунок:
string — текст;
number — число;
boolean — логічне значення.
Для перевірки на runtime відповідні декоратори мають бути додані явно:
import { IsBoolean, IsNumber, IsString } from 'class-validator';
export class CreateTaskDto {
@IsString()
title: string;
@IsBoolean()
completed: boolean;
@IsNumber()
priority: number;
}Масив описують за допомогою тип[] або Array<тип>:
class CreateArticleDto {
title: string;
tags: string[];
}Для валідації масиву рядків:
import { IsArray, IsString } from 'class-validator';
export class CreateArticleDto {
@IsString()
title: string;
@IsArray()
@IsString({ each: true })
tags: string[];
}Опція { each: true } означає, що @IsString() потрібно застосувати до кожного елемента масиву.
Якщо властивість може мати лише кілька конкретних значень, використовуйте union type:
type UserRole = 'admin' | 'editor' | 'viewer';
interface User {
name: string;
role: UserRole;
}
const user: User = {
name: 'Олена',
role: 'editor',
};Значення, якого немає в об’єднанні, TypeScript відхилить:
const user: User = {
name: 'Олена',
// Помилка: значення не входить до UserRole
role: 'owner',
};У DTO union type можна поєднати з @IsEnum():
import { IsEnum, IsString } from 'class-validator';
export enum UserRole {
ADMIN = 'admin',
EDITOR = 'editor',
VIEWER = 'viewer',
}
export class CreateUserDto {
@IsString()
name: string;
@IsEnum(UserRole)
role: UserRole;
}Enum корисний, коли той самий набір значень потрібен і TypeScript, і runtime-валідації.
Дати в JSON зазвичай надходять як рядки. Тому DTO часто описує їх як string і додатково перевіряє формат:
import { IsDateString, IsString } from 'class-validator';
export class CreateEventDto {
@IsString()
title: string;
@IsDateString()
startsAt: string;
}Після валідації startsAt залишається рядком. Якщо застосунку потрібен об’єкт Date, його можна перетворити в сервісному або іншому відповідному шарі:
const startsAt = new Date(dto.startsAt);Якщо DTO містить вкладений об’єкт, для нього створюють окремий клас:
import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
export class AddressDto {
@IsString()
city: string;
@IsString()
street: string;
}
export class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}Тут:
AddressDto описує адресу;
CreateUserDto містить адресу як вкладений об’єкт;
@ValidateNested() вмикає валідацію вкладеного DTO;
@Type(() => AddressDto) допомагає class-transformer створити екземпляр потрібного класу.
Без @ValidateNested() вкладені властивості можуть не перевірятися так, як очікується.
DTO передають у тип параметра методу контролера:
import { Body, Controller, Post } from '@nestjs/common';
import { IsEmail, IsString, Length } from 'class-validator';
export class CreateUserDto {
@IsString()
@Length(2, 50)
name: string;
@IsEmail()
email: string;
}
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
return {
name: dto.name,
email: dto.email,
};
}
}Властивість dto має тип CreateUserDto, тому редактор коду та компілятор знають:
які поля доступні;
які типи вони мають;
які поля є обов’язковими;
які методи можна викликати для цих значень.
Повний мінімальний приклад DTO, контролера та модуля:
// users.dto.ts
import { IsEmail, IsString, Length } from 'class-validator';
export class CreateUserDto {
@IsString()
@Length(2, 50)
name: string;
@IsEmail()
email: string;
}// users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './users.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
return {
message: 'Користувача створено',
user: dto,
};
}
}// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class UsersModule {}Після підключення UsersModule і глобального ValidationPipe запит із правильними даними буде прийнятий:
{
"name": "Олена",
"email": "olena@example.com"
}А запит із неправильним типом або некоректною електронною адресою буде відхилений:
{
"name": 42,
"email": "not-an-email"
}Обидва варіанти описують форму даних, але мають різне призначення.
Використовуйте інтерфейс, коли потрібен лише TypeScript-контракт:
export interface UserRecord {
id: number;
name: string;
email: string;
}Інтерфейс підходить для:
типізації результатів сервісу;
опису внутрішніх об’єктів;
контрактів між функціями;
типів, які не потребують runtime-валідації.
Використовуйте клас для вхідних HTTP-даних, якщо потрібно:
застосувати декоратори class-validator;
виконати runtime-валідацію;
використати class-transformer;
передати DTO у NestJS як runtime-значення.
export class CreateUserDto {
name: string;
email: string;
}Важливо не плутати типобезпечність компілятора з перевіркою зовнішніх даних:
TypeScript захищає код під час розробки, а
class-validatorперевіряє фактичні дані під час виконання.
Нижче наведено приклад DTO для створення завдання. Він використовує рядок, enum, число, необов’язкову властивість і масив:
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsInt,
IsOptional,
IsString,
Length,
Max,
Min,
} from 'class-validator';
export enum TaskStatus {
TODO = 'todo',
IN_PROGRESS = 'in_progress',
DONE = 'done',
}
export class CreateTaskDto {
@IsString()
@Length(3, 100)
title: string;
@IsOptional()
@IsString()
@Length(0, 500)
description?: string;
@IsEnum(TaskStatus)
status: TaskStatus;
@Type(() => Number)
@IsInt()
@Min(1)
@Max(5)
priority: number;
@IsArray()
@IsString({ each: true })
tags: string[];
}Такий DTO описує очікувану структуру:
const task: CreateTaskDto = {
title: 'Підготувати звіт',
status: TaskStatus.IN_PROGRESS,
priority: 3,
tags: ['work', 'report'],
};description можна не передавати, тому що він позначений як необов’язковий:
const taskWithoutDescription: CreateTaskDto = {
title: 'Підготувати звіт',
status: TaskStatus.TODO,
priority: 2,
tags: [],
};Водночас відсутність обов’язкової властивості буде помилкою TypeScript:
const invalidTask: CreateTaskDto = {
title: 'Підготувати звіт',
status: TaskStatus.TODO,
// Помилка: відсутні priority і tags
};interface CreateUserDto {
name: string;
}Інтерфейс не можна використовувати з декораторами та runtime-валідацією. Для HTTP DTO створіть клас:
class CreateUserDto {
name: string;
}anyclass CreateUserDto {
data: any;
}any вимикає перевірку типів. Краще описати точну структуру:
interface MetadataDto {
source: string;
version: number;
}
class CreateUserDto {
metadata: MetadataDto;
}@IsOptional()Знак ? повідомляє про необов’язковість TypeScript, але сам по собі не налаштовує поведінку class-validator:
class UpdateUserDto {
@IsString()
name?: string;
}Правильний варіант:
class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
}Типізація параметра:
create(@Body() dto: CreateUserDto) {
// ...
}не гарантує, що клієнт надіслав правильні дані під час виконання. Для цього потрібно налаштувати ValidationPipe і декоратори валідації.
Для вкладених DTO недостатньо лише вказати тип:
class CreateOrderDto {
customer: CustomerDto;
}Щоб вкладений об’єкт коректно перетворювався і перевірявся, використовуйте:
@ValidateNested()
@Type(() => CustomerDto)
customer: CustomerDto;DTO описує дані, які передаються між частинами NestJS-застосунку.
Інтерфейси забезпечують типізацію під час компіляції, але не доступні під час виконання.
Класи DTO можна використовувати з class-validator і class-transformer.
? робить властивість необов’язковою, а readonly забороняє її зміну в TypeScript.
Union types і enum обмежують властивість визначеним набором значень.
Для масивів і вкладених об’єктів потрібні відповідні типи та декоратори валідації.
TypeScript перевіряє код під час розробки, а ValidationPipe і class-validator захищають застосунок від некоректних даних під час виконання.