36 lines
1.5 KiB
Markdown
36 lines
1.5 KiB
Markdown
# 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.
|