Опирается на правила:
R-VLD-GRP-1…R-VLD-GRP-2иR-VLD-GRP-X1…R-VLD-GRP-X2из Validation Style Guide → раздел 4. Validation groups.
Важно знать
- Validation groups — механизм «один класс DTO, разные required-поля в разных сценариях». Не для всего подряд.
- Кейс:
OrderRequestдляPOST /orders(создание) иPATCH /orders/{id}(частичное обновление). На createcustomerIdобязателен; на update — нет.- В NestJS groups — строковые константы (
'OnCreate','OnUpdate'). Класс-маркер как в Java здесь не нужен.- На контроллере:
@UsePipes(new ValidationPipe({ groups: ['OnCreate'] }))на конкретном методе.- На декораторе:
@IsNotEmpty({ groups: ['OnCreate'] })— правило применяется только в указанной группе.- Применять узко. Только когда DTO реально один. Обычно — два разных DTO:
CreateOrderRequestиUpdateOrderRequest.- В NestJS-проекте с code-first подходом groups встречаются редко: разные эндпоинты — разные DTO-классы.
Validation groups — самый редко используемый механизм в проекте. Дефолтная позиция: два разных DTO лучше в 90% случаев. Раскрытие раздела 4 гайда.
Когда применяем
R-VLD-GRP-1: тот же класс DTO нужен в двух и более сценариях с разными required-полями.
Канонический пример — частичное обновление через PATCH:
// Константы групп — документированные, не магические строки (R-VLD-GRP-2)
export const ValidationGroup = {
CREATE: 'OnCreate',
UPDATE: 'OnUpdate',
} as const;
export class OrderRequest {
@IsUUID({ groups: [ValidationGroup.CREATE] }) // required только при создании
customerId?: string;
@IsString()
@IsNotEmpty() // required всегда (без groups = 'default')
@MaxLength(1000)
comment: string;
@ValidateNested({ each: true })
@Type(() => OrderItemRequest)
@ArrayMinSize(1, { groups: [ValidationGroup.CREATE] }) // минимум 1 при создании
items: OrderItemRequest[];
}
Контроллер — ValidationPipe на конкретном методе:
@Controller('orders')
export class OrderController {
@Post()
@UsePipes(new ValidationPipe({
whitelist: true,
transform: true,
groups: [ValidationGroup.CREATE],
exceptionFactory: (errors) => new InputValidationError(formatViolations(errors)),
}))
create(@Body() req: OrderRequest) {
// customerId проверяется @IsUUID({ groups: ['OnCreate'] })
}
@Patch(':id')
@UsePipes(new ValidationPipe({
whitelist: true,
transform: true,
groups: [ValidationGroup.UPDATE],
exceptionFactory: (errors) => new InputValidationError(formatViolations(errors)),
}))
update(@Param('id') id: string, @Body() req: OrderRequest) {
// customerId не проверяется (группа OnUpdate)
// comment по-прежнему required (без groups — применяется всегда)
}
}
Важная тонкость: constraint без groups принадлежит группе 'default'. new ValidationPipe({ groups: ['OnCreate'] }) применит только 'OnCreate'-правила и 'default'-правила вместе. Для включения обеих групп: groups: ['default', 'OnCreate'].
Маркер-группа — строковая константа
R-VLD-GRP-2: group — именованная константа, не магическая строка по месту.
// ХОРОШО — константы в общем файле
export const ValidationGroup = {
CREATE: 'OnCreate',
UPDATE: 'OnUpdate',
CONFIRM: 'OnConfirm',
} as const;
// ПЛОХО — магические строки разбросаны по файлам
@IsNotEmpty({ groups: ['create'] }) // 'create' или 'OnCreate'?
@IsNotEmpty({ groups: ['Create'] }) // дублирует, рассинхронизируется
В Java groups — пустые интерфейсы-маркеры. В TypeScript строки удобнее и не требуют interface. Главное — они именованные и документированные, а не хардкодятся по месту.
Расположение: рядом с DTO, для которых используются.
src/order/
order-request.dto.ts
order-validation-groups.ts // export const ValidationGroup = { CREATE: 'OnCreate', ... }
Когда не применяем
R-VLD-GRP-X1: группы для «строгой/мягкой» валидации — антипаттерн.
// ПЛОХО — два «режима» одного DTO
export const ValidationLevel = { STRICT: 'strict', LOOSE: 'loose' } as const;
export class OrderRequest {
@IsNotEmpty({ groups: [ValidationLevel.STRICT, ValidationLevel.LOOSE] })
customerId?: string;
@IsNotEmpty({ groups: [ValidationLevel.STRICT] })
@MaxLength(100, { groups: [ValidationLevel.STRICT, ValidationLevel.LOOSE] })
comment?: string;
}
Если есть «строгий» и «мягкий» — это два разных намерения клиента: Draft (черновик) и Final (финальная заявка). Правильно:
// ХОРОШО — два DTO с ясным контрактом
export class DraftOrderRequest {
@IsOptional() @MaxLength(1000) comment?: string;
// customerId не обязателен, items не обязательны
}
export class CreateOrderRequest {
@IsUUID() customerId: string;
@ArrayMinSize(1) @ValidateNested({ each: true }) @Type(() => OrderItemRequest) items: OrderItemRequest[];
@IsOptional() @MaxLength(1000) comment?: string;
}
Два DTO, две OpenAPI-схемы, два маппера — каждый с понятным контрактом.
R-VLD-GRP-X2: цепочки groups: ['OnCreate', 'OnConfirm', 'OnPay'] — флаг, что класс обслуживает слишком много сценариев.
// ПЛОХО — три фазы lifecycle в одном DTO
export class OrderRequest {
@IsUUID({ groups: ['OnCreate'] }) customerId?: string;
@ArrayMinSize(1, { groups: ['OnCreate'] }) items?: OrderItemRequest[];
@IsNotEmpty({ groups: ['OnConfirm'] }) shippingAddress?: string;
@IsNotEmpty({ groups: ['OnPay'] }) paymentToken?: string;
}
// ХОРОШО — отдельные DTO per lifecycle-шаг
export class CreateOrderRequest { /* только поля создания */ }
export class ConfirmOrderRequest { /* только поля подтверждения */ }
export class PayOrderRequest { /* только поля оплаты */ }
В NestJS-проекте групп почти нет
Code-first подход с отдельными DTO-классами на каждый эндпоинт устраняет большинство кейсов для groups:
// POST /orders → CreateOrderRequest
// PATCH /orders/:id → UpdateOrderRequest
// POST /orders/:id/confirm → ConfirmOrderRequest
// Каждый — отдельный класс с явными required-полями
export class UpdateOrderRequest {
@IsOptional() @ArrayMinSize(1) @ValidateNested({ each: true }) @Type(() => OrderItemRequest)
items?: OrderItemRequest[];
@IsOptional() @IsString() @MaxLength(1000)
comment?: string;
}
UpdateOrderRequest — отдельный класс, все поля @IsOptional(). Никаких groups, никакой магии.
Validation groups применяются в handcrafted-сценариях: административные эндпоинты, тестовые утилиты, внутренние конфиги — где нет строгой OpenAPI-схемы на каждую операцию.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Группы для «строгой/мягкой» валидации | R-VLD-GRP-X1 | Два отдельных DTO с разными правилами |
Цепочки groups: ['OnCreate', 'OnConfirm', 'OnPay'] | R-VLD-GRP-X2 | Отдельные DTO per lifecycle-шаг |
Магические строки 'create' / 'Create' без константы | R-VLD-GRP-2 | Именованная константа ValidationGroup.CREATE |
| Groups в DTO, которые лучше разделить на два класса | — | Два класса с ясными контрактами |
Куда дальше
- Validation → раздел 4. Validation groups — нормативные формулировки
R-VLD-GRP-*. - Где валидировать — глобальный
ValidationPipevs метод-уровень. - OpenAPI и DTO в NestJS — code-first: отдельные DTO-классы вместо groups.
- Cross-field validation — механизм, который путают с groups.