# ADR-0006: Zod schemas shared between API and frontends - **Status:** Accepted - **Date:** 2026-08-11 ## Context Validation rules exist twice by default: once in the API and once in the form. They drift, and the drift shows up as a form that accepts input the server rejects. ## Decision One Zod schema per input shape, defined in `@sport/validation` and imported by both sides. The API applies it through `ZodValidationPipe`; the frontends apply the same object to their forms. Zod was chosen over class-validator specifically because a class with decorators cannot cross into a React form, whereas a schema object can. Scope is deliberately limited to shape and format rules. Anything requiring database state — "is this coupon still valid", "is this variant in stock" — is a business rule and lives in the backend service layer. ## Consequences A rule change happens once. Field-level errors come back keyed by dotted path (`items.0.quantity`), which forms consume directly. The parsed output carries coercions and defaults, so controllers receive clean typed data. The discipline required is keeping business rules out of the schemas; a validation package that starts querying is a validation package that can no longer be shared. ## Alternatives considered class-validator + class-transformer, the NestJS default — rejected: not shareable with the frontends. Duplicating rules with a test to keep them in sync — rejected: the test tells you about drift after it has already shipped.