Skip to content

API Zod Request Validation

Objective

Use shared schemas from @repo/schemas as the single source of truth for both request validation and request types.

Canonical Pattern

Import the schema and inferred type directly, then validate at @Body():

ts
import {
  registerUserSchema,
  type RegisterUserDto,
} from '@repo/schemas/identity';
import { ZodValidationPipe } from '@api/shared/infrastructure/pipes/zod-validation.pipe';

@Post('register')
async register(
  @Body(new ZodValidationPipe(registerUserSchema)) dto: RegisterUserDto,
): Promise<{ message: string }> {
  // ...
}

The same pipe applies to @Query() when the whole query object should be validated — for example GET /agency/id uses getAgencyIdQuerySchema from @repo/schemas/identity for ?slug= / ?hostname= mutual exclusivity and hostname shape.

Rules

  • Do not create class DTO wrappers only for validation metadata.
  • Do not duplicate DTO interfaces in apps/api when the type already exists in @repo/schemas.
  • Prefer endpoint-explicit validation with @Body(new ZodValidationPipe(schema)).
  • Keep schemas and inferred DTO types in packages/schemas and consume them from API controllers/handlers.

Error Behavior

  • Invalid payloads return 400 Bad Request.
  • The response detail comes from Zod issues produced by safeParse.

Migration Notes

  • Legacy @ZodSchema class-decorator validation is removed.
  • Existing endpoints should be migrated by replacing:
    • local class DTO import
    • @Body() dto: LocalDto
  • With:
    • schema + type imports from @repo/schemas/...
    • @Body(new ZodValidationPipe(schema)) dto: SharedDtoType