Query-параметры представляют собой один из самых нестабильных источников входных данных в HTTP-запросах. В отличие от body, где структура обычно строго фиксируется контрактом DTO, query-string формируется динамически, часто пользователями или сторонними клиентами, и почти всегда приходит в виде строк. Это создаёт множество проблем: необходимость преобразования типов, обработка отсутствующих значений, защита от некорректных или вредоносных данных.
Библиотека class-validator в связке с class-transformer позволяет выстроить строгую схему проверки query-параметров на уровне DTO и автоматически привести входные данные к ожидаемым типам.
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 с декораторами 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 отвечает за проверку ограниченийБез явного преобразования все значения остаются строками, что приводит к ошибкам при проверке:
"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-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 валидация 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);
}
Даты также приходят строками:
?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;
При сложной логике применяются кастомные валидаторы.
Пример: проверка, что 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;
Без @Transform числа и булевы значения остаются
строками, что приводит к провалу @IsInt() и
@IsBoolean().
Без @IsOptional() отсутствующие параметры вызывают
ошибки даже при корректной логике.
Типичная ошибка:
@IsArray()
ids: number[];
без преобразования строки в массив.
@Type(() => FilterDto)
filter: FilterDto;
Без него вложенные структуры остаются обычными объектами без трансформации.
При включённом ValidationPipe возможны варианты
поведения:
Строгость поведения определяется конфигурацией пайпа.
Эффективная схема работы строится на трёх уровнях:
class-transformer)class-validator)Эта модель позволяет полностью формализовать входные данные и исключить неконтролируемые состояния в бизнес-логике.