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

Каждый раз, когда ваш сервис получает данные от клиента, возникает вопрос: «А вдруг там придёт мусор?». Строка вместо числа, отрицательная цена, пустое имя — всё это нужно отсечь до того, как данные попадут в логику. 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 пытается привести примитивные типы. Например, если в строке запроса пришло ?page=2 (строка), а DTO объявляет @IsInt() page: 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 реальный экземпляр класса и приводит примитивные типы.
  • Свой pipe пишется через PipeTransform и применяется точечно к параметру или методу.
  • Pipe отвечает за формат данных; бизнес-правила — за допустимость операции. Смешивать не стоит.

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

  • Контроллеры и роутинг — как NestJS сопоставляет маршруты методам.
  • Exception filters — как перевести исключения pipe и контроллера в HTTP-ответы.
  • Guards: аутентификация и авторизация — следующий рубеж после валидации.