Skip to content

ADR-0016: Validation Strategy — class-validator ​

Date: 2026-07-29 Status: Accepted

Context ​

The NestJS backend uses class-validator + class-transformer for request validation, integrated via a custom decorator system (WalnutAdminDecoratorField*). The frontend has no runtime schema validation layer.

Industry consensus (see 行业调研:Vue3 + NestJS 全栈架构) favors Zod for fullstack monorepos: a single schema serves both backend validation and frontend form validation, with automatic TypeScript type inference (z.infer<typeof schema>).

Two paths exist:

  1. Migrate to Zod — replace class-validator DTOs with Zod schemas, add ZodValidationPipe in NestJS
  2. Stay with class-validator — keep existing decorator system, acknowledge the trade-off

Decision ​

Chosen: Stay with class-validator + custom decorator system. Do not introduce Zod at this time.

Rationale ​

  1. The existing decorator system already provides "single declaration" semantics.WalnutAdminDecoratorField* decorators combine validation (class-validator) + transformation (class-transformer) + Swagger documentation into one decorator. This achieves a similar DX to Zod schemas (one declaration = validation + types + docs).

  2. Migration cost is disproportionate to current benefit. The validation system is deeply embedded across the codebase:

    • 16 guard classes depend on the DTO/validation pipeline
    • Middleware and interceptor layers expect class-validator error formats
    • libs/decorators/ contains 6 sub-systems (field/params/query/transformer/validator/swagger) with ~30 decorator files
    • All existing DTOs (~100+ classes) would need rewriting
  3. Frontend form validation reuse is not yet a pain point. The primary benefit of Zod (same schema on frontend and backend) requires frontend form libraries (VeeValidate + Zod). Without this integration, Zod on the backend alone adds dependency overhead without the "DRY validation" payoff.

  4. Zod introduces a second validation paradigm. Even a gradual migration means maintaining two validation systems side-by-side — class-validator for existing modules, Zod for new ones. This bifurcation increases cognitive load for contributors.

Trigger Conditions for Re-evaluation ​

Revisit this decision when any of these conditions are met:

TriggerWhy it changes the calculus
Frontend adopts VeeValidate + Zod for form validationDRY validation across frontend and backend becomes achievable
A new greenfield NestJS module is created from scratchOpportunity to try Zod without migration cost
class-validator becomes unmaintained or incompatible with a NestJS major versionForced migration
The project grows to 3+ frontend apps sharing validation with the same backendShared schema ROI increases

Alternatives considered ​

  • Migrate to Zod (replace class-validator DTOs with Zod schemas, add a ZodValidationPipe in NestJS) —— the migration cost is disproportionate to the current benefit. The WalnutAdminDecoratorField* decorators already fold validation (class-validator), transformation (class-transformer) and Swagger documentation into a single declaration; 16 guard classes plus the middleware / interceptor layers depend on the existing pipeline's error format; all 6 sub-systems under libs/decorators/ (~30 decorator files) and ~100+ DTO classes would have to be rewritten. And because the frontend has not adopted VeeValidate + Zod, the "one schema shared by frontend and backend" payoff is not reachable — Zod on the backend alone only adds a second validation paradigm.

Consequences ​

  • Validation logic remains backend-only (no frontend schema reuse)
  • DTO files continue using class-validator decorators + RealPickType / RealPartialType helpers
  • New NestJS modules follow the existing WalnutAdminDecoratorField* pattern
  • All existing libs/decorators/ infrastructure is preserved
  • If a trigger condition is met, start with a single module as proof-of-concept before committing to full migration

基于 MIT 许可发布