NestJS pipes

Роль pipes в архитектуре обработки запросов

Pipes представляют собой промежуточный слой в цепочке обработки входящих данных. Их основная задача заключается в трансформации и валидации аргументов, поступающих в контроллеры. Они работают до вызова метода обработчика маршрута и позволяют централизованно управлять качеством входных данных.

Pipes применяются к:

  • параметрам маршрута (@Param)
  • query-параметрам (@Query)
  • телу запроса (@Body)
  • заголовкам (@Headers)
  • произвольным аргументам методов контроллеров

Ключевая особенность заключается в том, что pipes выполняются до бизнес-логики, что позволяет отделить проверку данных от основной функциональности.


Контракт PipeTransform

Любой pipe реализует интерфейс PipeTransform, определяющий единый контракт обработки:

interface PipeTransform<T = any, R = any> {
  transform(value: T, metadata: ArgumentMetadata): R;
}

Где:

  • value — входное значение
  • metadata — информация о параметре (тип, имя, источник)
  • R — результат преобразования или исключение

Структура ArgumentMetadata:

  • type: "body" | "query" | "param" | "custom"
  • metatype: тип данных (например, класс DTO)
  • data: имя параметра

Встроенные pipes

NestJS предоставляет набор стандартных pipes для типовых задач.

ParseIntPipe

Используется для преобразования строки в число:

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.service.findOne(id);
}

Если преобразование невозможно, выбрасывается исключение BadRequestException.


ParseBoolPipe

Преобразует строковые значения в булевы:

@Get()
getFlag(@Query('enabled', ParseBoolPipe) enabled: boolean) {
  return enabled;
}

Поддерживаются значения true, false, 1, 0.


ParseArrayPipe

Используется для преобразования строкового списка в массив:

@Get()
find(@Query('ids', ParseArrayPipe) ids: number[]) {
  return ids;
}

DefaultValuePipe

Назначает значение по умолчанию:

@Get()
list(@Query('page', new DefaultValuePipe(1)) page: number) {
  return page;
}

ValidationPipe

Один из ключевых pipes в экосистеме. Используется для валидации DTO на основе декораторов.

Пример DTO:

import { IsString, IsInt } fr om 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;
}

Использование:

@Post()
create(@Body(new ValidationPipe()) dto: CreateUserDto) {
  return this.service.create(dto);
}

Глобальный ValidationPipe

Часто применяется на уровне приложения:

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

Параметры:

  • whitelist — удаляет неизвестные поля
  • transform — автоматически преобразует типы
  • forbidNonWhitelisted — выбрасывает ошибку при лишних полях
  • disableErrorMessages — скрывает детали ошибок

Трансформация данных

Pipes могут не только валидировать, но и изменять данные.

Пример кастомного преобразования строки в объект:

@Injectable()
export class ParseJsonPipe implements PipeTransform {
  transform(value: string) {
    try {
      return JSON.parse(value);
    } catch {
      throw new BadRequestException('Invalid JSON');
    }
  }
}

Создание кастомных pipes

Кастомный pipe реализуется через PipeTransform.

Пример проверки диапазона:

@Injectable()
export class RangePipe implements PipeTransform {
  constructor(private min: number, private max: number) {}

  transform(value: any) {
    const num = Number(value);

    if (isNaN(num)) {
      throw new BadRequestException('Value must be a number');
    }

    if (num < this.min || num > this.max) {
      throw new BadRequestException('Value out of range');
    }

    return num;
  }
}

Использование:

@Get()
getValue(@Query('val', new RangePipe(1, 100)) val: number) {
  return val;
}

Параметризация кастомных pipes

Nest не поддерживает DI напрямую в параметризованных конструкторах pipes через декораторы параметров, поэтому часто используется фабричный подход:

export const Range = (min: number, max: number) =>
  new RangePipe(min, max);

Асинхронные pipes

Pipe может возвращать Promise, что позволяет выполнять асинхронные операции:

@Injectable()
export class AsyncCheckPipe implements PipeTransform {
  async transform(value: any) {
    const exists = await this.service.exists(value);

    if (!exists) {
      throw new BadRequestException('Not found');
    }

    return value;
  }
}

Порядок выполнения pipes

Pipes выполняются:

  1. глобальные pipes
  2. pipes контроллера
  3. pipes метода
  4. pipes параметров

Этот порядок позволяет комбинировать уровни проверки и трансформации.


Область применения и binding

Pipes могут быть привязаны:

К параметру

@Param('id', ParseIntPipe) id: number

К методу

@Post()
@UsePipes(ValidationPipe)
create(@Body() dto: CreateUserDto) {}

К контроллеру

@UsePipes(ValidationPipe)
@Controller('users')
export class UserController {}

Глобально

app.useGlobalPipes(new ValidationPipe());

Ошибки и исключения в pipes

Pipes используют механизм исключений NestJS для прерывания выполнения цепочки обработки.

Чаще всего используется:

  • BadRequestException
  • UnprocessableEntityException

Пример:

throw new BadRequestException('Invalid value');

Совместная работа с class-validator

ValidationPipe тесно интегрирован с библиотекой class-validator и class-transformer.

Пример DTO с трансформацией:

import { Type } from 'class-transformer';
import { IsNumber } from 'class-validator';

export class QueryDto {
  @Type(() => Number)
  @IsNumber()
  lim it: number;
}

Производительность pipes

Pipes выполняются на раннем этапе запроса, поэтому:

  • минимизация логики внутри pipes критична
  • тяжёлые операции (например, запросы в БД) следует оптимизировать или кешировать
  • глобальные pipes влияют на каждый запрос и требуют аккуратной настройки

Типовые архитектурные ошибки

  • использование pipes для бизнес-логики
  • выполнение сложных запросов внутри transform
  • отсутствие transform при необходимости типизации
  • дублирование валидации в контроллерах и сервисах
  • чрезмерное использование глобальных pipes без необходимости

Композиция pipes

Несколько pipes могут применяться последовательно:

@Param('id', ParseIntPipe, RangePipe(1, 100))
id: number

Каждый pipe получает результат предыдущего.


Расширенные сценарии использования

  • нормализация строк (trim, lowercase)
  • парсинг сложных структур query string
  • валидация токенов или API ключей
  • преобразование форматов дат
  • адаптация внешних API данных под внутренние DTO

Взаимодействие с Guards и Interceptors

Pipes выполняются до guards и interceptors на уровне аргументов контроллера. Это создаёт строгую цепочку:

  1. Middleware
  2. Guards
  3. Pipes
  4. Controller
  5. Interceptors (response mapping)
  6. Exception filters