Всем привет! Наткнулся недавно на статью Переосмысление DTO в Java и решил интерпретировать предложенный подход к TypeScript и NestJS. Данный стек используется нашей командой повсеместно, отсюда и мой выбор.
Вспомним, что такое DTO в рамках фреймворка NestJS. Этот инструмент помогает описывать структуры данных передаваемых между клиентом и сервером, а также слоями приложения. Для построения такого рода объектов чаще всего используется класс с описанными полями и валидацией. Вроде ничего сложного.
Проблемы начинаются в тот момент, когда для некой сущности в приложении появляется с десяток DTO'шек, что приводит нас к дублированию кода. Одни и те же поля вроде name, email, id повторяются из DTO в DTO, а внесение изменений превращается в рутинную правку десятков файлов.
В этой статье я попробую показать, как можно организовать DTO более модульно. Такой подход позволяет писать более читаемый, масштабируемый код с соблюдением DRY принципа. Начнем!
Базовая структура
Для начала рассмотрим привычный способ создания DTO. Возьмем для примера создание юзера:
export class CreateUserDto { name: string; email: string; password: string; }
После добавления валидации наш класс будет выглядеть следующим образом:
import { IsString, IsEmail, Length } from 'class-validator'; export class CreateUserDto { @IsString() @Length(2, 50) name: string; @IsEmail() email: string; @IsString() @Length(6, 100) password: string; }
Это читаемо и удобно, но до тех пор, пока не появляется UpdateUserDto, UserResponseDto, и другие варианты. Понятно, что в итоге мы получим дубликаты полей с одними и теми же правилами. И когда бизнес попросит нас что-то поменять, нам придется идти по всем этим файлам.
Возникает закономерный вопрос: что мы можем с этим сделать?
Путь к модульности через миксины
Обычные интерфейсы и implements в TypeScript не подходят для аннотаций валидации, таких как @IsEmail или @Length, потому что интерфейсы не существуют в рантайме. А class-validator использует именно рантайм-декораторы.
Дабы сохранить модульность и обеспечить рабочую валидацию, можно использовать функции-миксины. Это функции, которые принимают базовый класс и возвращают новый — с добавленным полем и декораторами.
Возьмем структуру данных с полями name, email и password. Вместо того чтобы повторять эти поля в каждом DTO, создадим для каждого отдельный класс:
// dto/fields/with-name.dto.ts import { IsString, Length } from 'class-validator'; export function WithName<T extends new (...args: any[]) => any>(Base: T) { abstract class WithName extends Base { @IsString() @Length(2, 50) name: string; } return WithName; } // dto/fields/with-email.dto.ts import { IsEmail } from 'class-validator'; export function WithEmail<T extends new (...args: any[]) => any>(Base: T) { abstract class WithEmail extends Base { @IsEmail() email: string; } return WithEmail; } // dto/fields/with-password.dto.ts import { IsString, Length } from 'class-validator'; export function WithPassword<T extends new (...args: any[]) => any>(Base: T) { abstract class WithPassword extends Base { @IsString() @Length(6, 100) password: string; } return WithPassword; }
Теперь чтобы описать тот же DTO для создания пользователя достаточно собрать его из строительных блоков:
// dto/create-user.dto.ts import { WithName } from './fields/with-name.dto'; import { WithEmail } from './fields/with-email.dto'; import { WithPassword } from './fields/with-password.dto'; export class CreateUserDto extends WithName(WithEmail(WithPassword(class {}))) {}
Все поля будут включены в финальный класс с валидацией. NestJS и class-validator увидят все декораторы, как будто они определены напрямую.
На этом простом примере видно, что такой подход дает явную структуру и единый источник правды для каждого поля. Но для новичков, не знакомых с миксинами, такая структура может быть неочевидной.
Заключение
Использование модульных DTO в TypeScript — это хороший способ улучшить структуру и поддерживаемость кода, особенно в больших проектах, где DTO могут повторяться во многих местах. Он позволит снизить вероятность ошибок при изменении, а также упростит процесс сопровождения.
А как вы организуете DTO в своих проектах? Используете ли подобный подход или предпочитаете что-то другое? Какие еще минусы видите в таком подходе?
P.S.: Это моя первая статья на Хабре, буду рад любой конструктивной критике :-)
