NestJS / 38 минут

DTO и ValidationPipe

Проверяйте входные DTO во время исполнения.

В процессе Теория: впереди Практика: впереди Тест: впереди
05 / 14
После этой главы
  • Проверяйте входные DTO во время исполнения.
  • Применить тему в задаче «DTO и ValidationPipe» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Типизация не проверяет сеть

TypeScript работает до запуска программы. Клиент может отправить число вместо имени, неизвестное поле, null или очень длинную строку независимо от типа параметра контроллера. DTO описывает внешний вход, а ValidationPipe и декораторы class-validator проверяют его во время исполнения. Проверенный DTO затем становится удобной границей сценария.

Для проверки нужен класс, существующий в JavaScript. Interface и type исчезают при компиляции, поэтому не предоставляют метаданные валидации. DTO не обязан совпадать с моделью БД: публичный вход создания книги не должен разрешать клиенту назначать ownerId, служебный статус и парольный хеш просто потому, что такие поля есть в хранилище.

TypeScript
import { IsString, Length } from 'class-validator';
class CreateBookDto {
  @IsString()
  @Length(1, 120)
  title!: string;
}

Включение общей границы

Глобальный pipe делает правила единообразными для маршрутов. whitelist удаляет свойства, для которых нет валидирующих декораторов. forbidNonWhitelisted вместо тихого удаления отклоняет такой ввод. transform позволяет получить экземпляр DTO и использовать предусмотренные преобразования. Выбор настроек является частью контракта API, поэтому изменение строгого отказа на удаление может повлиять на клиентов.

TypeScript
app.useGlobalPipes(new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
}));

Обязательно проверьте маршрут через HTTP. Если передавать DTO напрямую из unit-теста в метод контроллера, глобальный pipe не выполнится. Отдельный тест самого ValidationPipe полезен для граничных значений, но не доказывает правильность его регистрации в приложении. Эти уровни проверки дополняют друг друга.

Преобразование должно быть явным

Преобразование экземпляра DTO не означает безопасное угадывание смысла каждой строки. Параметры URL приходят текстом; для числа используйте ParseIntPipe или осознанное преобразование class-transformer с дальнейшими ограничениями. Особенно осторожно обращайтесь с boolean: JavaScript считает непустую строку "false" истинной при Boolean(value).

TypeScript
import { Type } from 'class-transformer';
import { IsInt, Min, Max } from 'class-validator';
class PageDto {
  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(1000)
  page!: number;
}

Числовой диапазон ограничивает стоимость запроса и выражает смысл значения. Для денежных сумм дополнительно определите единицы и способ хранения; дробное число IEEE 754 не заменяет денежную модель. Для строк решите, нужна ли нормализация пробелов. Length(1) пропускает строку из пробела: если нужны непустые содержательные имена, добавьте отдельное правило или нормализуйте вход по явно описанному контракту.

Необязательность и вложенные структуры

IsOptional пропускает проверку для отсутствующего значения и null; это может отличаться от требования «можно не передать, но нельзя передать null». Для точного поведения используйте подходящее условие ValidateIf и отдельные тесты. При PATCH отличайте отсутствие поля от явной команды очистить его. Нельзя превращать оба случая в одно значение случайным оператором default.

Вложенные DTO требуют ValidateNested и информации о типе для class-transformer. Массив нуждается в проверке самого массива, количества элементов и каждого элемента. При ограничении тела HTTP по байтам всё равно нужны ограничения строк и коллекций по смыслу. Валидное по размеру тело может содержать чрезмерно дорогой для сценария запрос.

Ошибка ввода и бизнес-правило

Валидация структуры отвечает на вопрос, можно ли интерпретировать запрос. Проверка уникальности email, существования книги и разрешения пользователя требует состояния приложения и относится к сценарию. Даже асинхронный валидатор не заменяет уникальное ограничение БД: между проверкой и вставкой может пройти конкурентный запрос.

TypeScript
class InputDto {
  @IsString()
  @Length(1, 40)
  name!: string;
}

Такой DTO не удостоверяет личность человека и не доказывает, что имя уникально. Не возвращайте клиенту внутренние сообщения БД при конфликте. Ограничьте структуру ошибок понятными полями и кодами, а неожиданные сбои сохраняйте в защищённых логах без тела с паролями. Приложение не должно раскрывать stack trace из-за некорректного JSON.

Практика ValidationPipe

В задаче проверьте объект с единственным полем name: строка длиной от одного до сорока символов. Неизвестные поля запрещены. solve возвращает true для принятого значения и false для отказа, включая null, массив и примитив. Выполните проверку настоящим ValidationPipe с metatype DTO; обработайте ожидаемое исключение в границе упражнения.

Сначала перечислите классы входов: корректная строка, пустая, слишком длинная, число, отсутствующее свойство и лишнее поле. Не подменяйте class-validator условием только typeof: цель главы — настроить реальную runtime-границу. После решения подключите этот же подход глобально в проекте и сравните HTTP-ответы для всех перечисленных случаев.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0