Валидация query-параметров

Query-параметры представляют собой один из самых нестабильных источников входных данных в HTTP-запросах. В отличие от body, где структура обычно строго фиксируется контрактом DTO, query-string формируется динамически, часто пользователями или сторонними клиентами, и почти всегда приходит в виде строк. Это создаёт множество проблем: необходимость преобразования типов, обработка отсутствующих значений, защита от некорректных или вредоносных данных.

Библиотека class-validator в связке с class-transformer позволяет выстроить строгую схему проверки query-параметров на уровне DTO и автоматически привести входные данные к ожидаемым типам.


Особенности query-параметров как источника данных

Query-параметры имеют ряд характерных особенностей:

  • Все значения приходят как строки
  • Отсутствие строгой структуры (ключи могут отсутствовать или быть лишними)
  • Возможны повторяющиеся параметры (?id=1&id=2)
  • Поддержка массивов зависит от фреймворка
  • Высокая вероятность некорректного ввода

Типичный пример запроса:

GET /users?limit=10&page=2&active=true&role=admin

Фактические значения на уровне HTTP:

{
  "limit": "10",
  "page": "2",
  "active": "true",
  "role": "admin"
}

Даже числовые и булевые значения представлены строками, что требует преобразования.


Базовая модель DTO для query-параметров

Использование DTO с декораторами class-validator позволяет описать структуру query-параметров декларативно.

import { IsInt, IsOptional, IsBoolean, IsString, Min, Max } fr om 'class-validator';
import { Transform } fr om 'class-transformer';

export class GetUsersQueryDto {
  @IsOptional()
  @Transform(({ value }) => parseInt(value, 10))
  @IsInt()
  @Min(1)
  page: number;

  @IsOptional()
  @Transform(({ value }) => parseInt(value, 10))
  @IsInt()
  @Min(1)
  @Max(100)
  lim it: number;

  @IsOptional()
  @Transform(({ value }) => value === 'true')
  @IsBoolean()
  active: boolean;

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

Ключевой момент заключается в том, что валидация и трансформация разделяются:

  • class-transformer отвечает за приведение типов
  • class-validator отвечает за проверку ограничений

Преобразование типов query-параметров

Без явного преобразования все значения остаются строками, что приводит к ошибкам при проверке:

"10" // string вместо number
"true" // string вместо boolean

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

@Transform(({ value }) => parseInt(value, 10))
@IsInt()
page: number;

Для булевых значений часто применяется явное сравнение:

@Transform(({ value }) => value === 'true')
@IsBoolean()
active: boolean;

Альтернативный подход — универсальная функция преобразования:

const toBoolean = ({ value }) => {
  if (value === 'true') return true;
  if (value === 'false') return false;
  return value;
};

Обработка необязательных параметров

Query-параметры почти всегда являются опциональными. Для этого используется декоратор:

@IsOptional()

Он предотвращает выполнение остальных валидаторов, если значение отсутствует.

Пример:

@IsOptional()
@IsString()
search?: string;

Без IsOptional() отсутствие параметра приведёт к ошибке валидации.


Валидация строковых параметров

Для строковых параметров применяются базовые ограничения:

@IsOptional()
@IsString()
@Length(3, 50)
username?: string;

Дополнительно могут использоваться:

  • @Matches() — регулярные выражения
  • @IsEnum() — перечисления
  • @IsUUID() — идентификаторы

Пример с enum:

enum SortOrder {
  ASC = 'asc',
  DESC = 'desc',
}

@IsOptional()
@IsEnum(SortOrder)
sort: SortOrder;

Числовые параметры и ограничения диапазонов

Типичные параметры пагинации:

@Transform(({ value }) => parseInt(value, 10))
@IsInt()
@Min(1)
page: number;

@Transform(({ value }) => parseInt(value, 10))
@IsInt()
@Min(1)
@Max(100)
lim it: number;

Особое значение имеют ограничения:

  • @Min() предотвращает отрицательные и нулевые значения
  • @Max() ограничивает нагрузку на сервер
  • @IsInt() исключает дробные значения

Работа с массивами в query-параметрах

Массивы в query-string могут передаваться разными способами:

?ids=1&ids=2&ids=3

или

?ids=1,2,3

Для обработки используется трансформация:

import { Type } fr om 'class-transformer';
import { IsArray, IsInt } from 'class-validator';

export class QueryDto {
  @Transform(({ value }) =>
    typeof value === 'string' ? value.split(',').map(Number) : value
  )
  @IsArray()
  @IsInt({ each: true })
  ids: number[];
}

Проверка каждого элемента массива осуществляется через:

@IsInt({ each: true })

Глубокая трансформация и вложенные структуры

В некоторых случаях query-параметры могут содержать вложенные структуры:

?filter[name]=john&filter[age]=30

DTO:

import { Type } from 'class-transformer';
import { IsString, IsInt } from 'class-validator';

class FilterDto {
  @IsString()
  name: string;

  @Transform(({ value }) => parseInt(value, 10))
  @IsInt()
  age: number;
}

export class QueryDto {
  @Type(() => FilterDto)
  filter: FilterDto;
}

Важно включение:

@Type(() => FilterDto)

Без этого вложенные объекты не будут корректно преобразованы.


Интеграция с NestJS ValidationPipe

В NestJS валидация query-параметров активируется через ValidationPipe:

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

Ключевые параметры:

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

Контроллер:

@Get()
getUsers(@Query() query: GetUsersQueryDto) {
  return this.userService.find(query);
}

Обработка дат в query-параметрах

Даты также приходят строками:

?from=2024-01-01&to=2024-12-31

DTO:

@IsOptional()
@IsDateString()
from?: string;

@IsOptional()
@IsDateString()
to?: string;

При необходимости преобразования в Date:

@Transform(({ value }) => new Date(value))
@IsDate()
from: Date;

Пользовательские валидаторы для query-параметров

При сложной логике применяются кастомные валидаторы.

Пример: проверка, что page и limit согласованы:

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

@ValidatorConstraint({ name: 'pagination', async: false })
export class PaginationConstraint implements ValidatorConstraintInterface {
  validate(_: any, args: ValidationArguments) {
    const obj = args.object as any;
    return obj.page * obj.lim it <= 10000;
  }

  defaultMessage() {
    return 'Слишком большой диапазон пагинации';
  }
}

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

@Validate(PaginationConstraint)
page: number;

Частые ошибки при валидации query-параметров

Отсутствие transform

Без @Transform числа и булевы значения остаются строками, что приводит к провалу @IsInt() и @IsBoolean().


Игнорирование IsOptional

Без @IsOptional() отсутствующие параметры вызывают ошибки даже при корректной логике.


Неправильная работа с массивами

Типичная ошибка:

@IsArray()
ids: number[];

без преобразования строки в массив.


Отсутствие Type-декоратора для вложенных объектов

@Type(() => FilterDto)
filter: FilterDto;

Без него вложенные структуры остаются обычными объектами без трансформации.


Поведение при некорректных query-параметрах

При включённом ValidationPipe возможны варианты поведения:

  • 400 Bad Request при нарушении правил
  • автоматическое удаление лишних полей (whitelist)
  • преобразование допустимых значений
  • полное отклонение запроса при критических ошибках

Строгость поведения определяется конфигурацией пайпа.


Сочетание class-validator и class-transformer в query-валидации

Эффективная схема работы строится на трёх уровнях:

  1. Получение query-string (все значения — строки)
  2. Трансформация в DTO (class-transformer)
  3. Проверка ограничений (class-validator)

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