Опирается на правила: R-VLD-GRP-1R-VLD-GRP-2 и R-VLD-GRP-X1R-VLD-GRP-X2 из Validation Style Guide → раздел 4. Validation groups.

Важно знать

  • Validation groups — механизм «один класс DTO, разные required-поля в разных сценариях». Не для всего подряд.
  • Кейс: OrderRequest для POST /orders (создание) и PATCH /orders/{id} (частичное обновление). На create customerId обязателен; на 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-*.
  • Где валидировать — глобальный ValidationPipe vs метод-уровень.
  • OpenAPI и DTO в NestJS — code-first: отдельные DTO-классы вместо groups.
  • Cross-field validation — механизм, который путают с groups.