← назад к разделу

NESTJS · VALIDATION Фаза 7. Валидация и pipes

Валидация и pipes в NestJS

TypeScript · NestJS

ValidationPipe, DTO, whitelist, transform и собственные правила входа.

1. Pipe отсекает неверный вход до метода

1 / 5

ValidationPipe получает значение аргумента, проверяет его и возвращает допустимое или ошибку. Бизнес-метод видит только проверенные данные.

  • Работает на границе контроллера.
  • Не заменяет доменные инварианты.
  • Может преобразовать тип.
Input ValidationPipevalidate + transform400 on violation Controller Valid

2. DTO описывает ожидаемую форму

2 / 5

class-validator объявляет @IsString, @IsEmail, @IsPositive и другие ограничения рядом с полями.

  • Обязательность видна в типе.
  • Ошибка содержит поле и причину.
  • Вложенный DTO валидируется каскадно.
CreateOrderDto @IsString() customerId @ArrayMinSize(1) items @ValidateNested + @Type(ItemDto)

3. whitelist отбрасывает неизвестные поля

3 / 5

whitelist: true удаляет свойства вне DTO, forbidNonWhitelisted: true отклоняет запрос. Это защита от mass assignment.

  • Не принимайте role и isAdmin от клиента.
  • Явно разрешайте поля.
  • Ошибки массива показывают путь.
Inputemail: "a@b.c"isAdmin: true whitelistemail keptisAdmin stripped Safe DTO

4. transform приводит типы на границе

4 / 5

HTTP передаёт строки. transform: true и @Type преобразуют числа, boolean, даты и вложенные DTO до валидации.

  • Не складывайте строки как числа.
  • enableImplicitConversion оценивается осторожно.
  • Ошибка преобразования возвращает 400.
"42" "true" transformnumber + booleantyped DTO Validated

5. Свой pipe решает узкую задачу

5 / 5

Реализация PipeTransform нормализует или проверяет вход. Системная уникальность и состояние БД остаются в use case.

  • Формат входа - pipe.
  • Инвариант состояния - домен.
  • Ошибка должна быть понятной клиенту.
SkuFormatPipesyntax only Use casestock + business rules Databasesource of truth Каждая проверка рядом со своим источником

← назад к разделу

Каждый раз, когда ваш сервис получает данные от клиента, возникает вопрос: «А вдруг там придёт мусор?». Строка вместо числа, отрицательная цена, пустое имя — всё это нужно отсечь до того, как данные попадут в логику. Pipe в NestJS — это именно та точка, где это происходит.

Что такое pipe и зачем он нужен

Раньше проверку данных писали прямо в теле метода:

@Post()
create(@Body() body: any) {
  if (!body.name || typeof body.price !== 'number' || body.price < 1) {
    throw new BadRequestException('Неверные данные');
  }
  // ... основная логика
}

С ростом числа полей это превращается в набор проверок, которые повторяются в каждом методе. Нарушение и хорошее описание ошибки — одно, а бизнес-логика — другое.

Pipe — это шаг перед методом контроллера. NestJS пропускает входные данные через pipe: тот либо возвращает их (возможно, преобразованными), либо бросает исключение — и тогда до метода запрос вообще не дойдёт.

Простейший встроенный pipe — ParseIntPipe. Параметр в пути всегда приходит строкой; pipe превращает её в число:

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';

@Controller('products')
export class ProductController {
  @Get(':id')
  async getOne(@Param('id', ParseIntPipe) id: number) {
    // id — уже число, не строка
  }
}

Если в пути вместо числа окажется abc, ParseIntPipe вернёт 400 Bad Request сразу, без входа в метод.

NestJS поставляется с несколькими готовыми pipes: ParseIntPipe, ParseBoolPipe, ParseUUIDPipe, ParseArrayPipe, DefaultValuePipe. Они закрывают частые случаи с параметрами маршрута и строки запроса.

ValidationPipe и DTO

Для проверки тела запроса нужен более мощный инструмент — ValidationPipe. Он работает в паре с двумя библиотеками:

  • class-validator — декораторы-правила для полей класса (@IsString, @IsInt, @Min, @MaxLength и ещё несколько десятков).
  • class-transformer — превращает «голый» JSON-объект в экземпляр класса, чтобы декораторы сработали.

Сначала установите их:

npm install class-validator class-transformer

Затем включите ValidationPipe глобально при старте приложения:

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({ whitelist: true, transform: true }),
  );
  await app.listen(3000);
}

Теперь создайте DTO-класс с правилами:

import { IsString, IsInt, Min, IsOptional, MaxLength } from 'class-validator';

export class CreateProductDto {
  @IsString()
  @MaxLength(200)
  name: string;

  @IsInt()
  @Min(1)
  price: number;

  @IsOptional()
  @IsString()
  category?: string;
}

И используйте его в контроллере:

@Post()
create(@Body() body: CreateProductDto) {
  // body уже проверен и является экземпляром CreateProductDto
}

Если клиент пришлёт price: -5 или не передаст name, NestJS автоматически ответит 400 с подробным описанием: какие поля не прошли проверку и почему.

Что делают whitelist и transform

Два параметра при создании ValidationPipe особенно важны.

whitelist: true — срезает из тела запроса все поля, которых нет в DTO. Если клиент передаст { name: "Кружка", price: 100, role: "admin" }, поле role просто исчезнет. Это не ошибка — NestJS молча отбрасывает лишнее. Благодаря этому нельзя случайно «протащить» неожиданное поле в логику.

Если хотите, чтобы при наличии лишних полей запрос сразу отклонялся ошибкой — добавьте forbidNonWhitelisted: true:

new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })

transform: true — включает class-transformer. Без него тело запроса остаётся обычным объектом (Object), и TypeScript-тип CreateProductDto — только подсказка. С transform: true NestJS реально создаёт экземпляр класса, поэтому методы DTO (если они есть) работают, и instanceof CreateProductDto вернёт true.

Кроме того, transform: true приводит примитивы в параметрах маршрута: @Param('id') id: number придёт числом и без ParseIntPipe. А вот поля DTO из строки запроса он сам не трогает: ?page=2 при @IsInt() page: number даст 400 с текстом «page must be an integer number» — в DTO попадёт строка "2". Чтобы строки превращались в числа и булевы значения, включают transformOptions: { enableImplicitConversion: true } при создании ValidationPipe либо помечают поле @Type(() => Number) из class-transformer.

Как написать свой pipe

Встроенных pipes хватает для большинства случаев. Если нужно нестандартное преобразование — напишите свой, реализовав интерфейс PipeTransform:

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: unknown): unknown {
    if (typeof value === 'string') {
      return value.trim();
    }
    return value;
  }
}

Применить его можно точечно — к одному параметру:

@Post()
create(@Body('name', TrimPipe) name: string) { ... }

Если pipe бросает исключение — NestJS останавливает запрос и возвращает соответствующий HTTP-ответ. Если возвращает значение — оно идёт дальше в метод.

Формат и бизнес-правило — два разных уровня

Важно понимать, за что отвечает pipe, а за что — нет.

Pipe и DTO проверяют формат: правильной ли формы данные? Строка ли имя, число ли цена, не слишком ли длинное поле? Это вопросы структуры запроса, и они решаются на границе до контроллера.

Бизнес-правила — это другое: можно ли создать продукт с таким именем, если такое имя уже занято? Достаточно ли остатков на складе? Это вопросы состояния системы, и их проверяют уже внутри — в слое логики, а не в DTO.

Смешивать уровни — частая ошибка. Когда в DTO-валидатор добавляют обращение к базе данных, граница размывается: DTO начинает знать о состоянии системы, а это делает его сложным и трудно тестируемым. Правило простое: DTO отвечает на «правильно ли выглядят данные?», а не на «допустима ли эта операция?».

Коротко

  • Pipe — шаг перед методом контроллера: преобразует или проверяет входные данные, может отклонить запрос до входа в метод.
  • Встроенные pipes (ParseIntPipe, ParseBoolPipe, ParseUUIDPipe и другие) закрывают типовые случаи с параметрами маршрута.
  • ValidationPipe + class-validator + class-transformer — стандартный способ валидировать тело запроса через DTO-класс с декораторами.
  • whitelist: true отбрасывает поля, которых нет в DTO; transform: true делает из JSON реальный экземпляр класса и приводит примитивы в параметрах пути, а для полей DTO из строки запроса нужен enableImplicitConversion или @Type.
  • Свой pipe пишется через PipeTransform и применяется точечно к параметру или методу.
  • Pipe отвечает за формат данных; бизнес-правила — за допустимость операции. Смешивать не стоит.

Что пощупать

Две границы проверки в одном сервисе практикума remodov/marketplace-system-nodejs: формат запроса ловят декораторы class-validator в DTO и ValidationPipe с exceptionFactory, который отдаёт 400 с именем поля в errors, а смысл (цена больше нуля, скидка не глубже половины, остаток не ниже резерва) проверяет сам класс Product. @IsDefined стоит отдельно, потому что @IsInt на пропавшем поле молчит.

Код: product.dto.ts, problem.ts, product.entity.ts.

Сделаем сами

Ветка step-03-commands-and-errors.

Что почитать дальше