All TypeScript templates

TypeScript + Zod Validation

Runtime validation at boundaries with Zod schemas and inferred types.

DevZone Tools2,280 copiesUpdated Mar 14, 2026TypeScript
# CLAUDE.md — TypeScript + Zod Validation

## Where to validate

- Validate at every **boundary**: HTTP request, HTTP response (third-party), DB row → object, env vars, message bus.
- Don't validate inside business logic — by then, types are guaranteed.
- One schema per shape. Co-locate with the code that consumes it.

## Schema basics

- Schemas live in `lib/validators/<feature>.ts`.
- Infer types from schemas — never declare types and schemas separately:
  ```ts
  export const UserSchema = z.object({
    id: z.string().uuid(),
    email: z.string().email(),
    role: z.enum(["admin", "member"]),
  })
  export type User = z.infer<typeof UserSchema>
  ```
- One source of truth: schema → type. If you find yourself writing the type by hand, you're probably duplicating.

## Parsing vs safeParsing

- Use `.parse()` when invalid input is unrecoverable (env vars at boot, internal contracts).
- Use `.safeParse()` at HTTP boundaries — return a typed error response on failure. Don't let zod throw across an HTTP handler.

## Composition

- Build complex schemas from smaller ones:
  ```ts
  const Address = z.object({ street: z.string(), city: z.string() })
  const User = z.object({ ..., address: Address })
  ```
- Reuse schemas across requests/responses with `.pick`, `.omit`, `.partial`, `.required`. Don't redeclare related shapes.

## Coercion & transforms

- `z.coerce.number()` for query strings (everything's a string in URLs).
- `.transform(...)` for derived shapes — but the input and output types should both be intentional.
- `.refine(...)` for cross-field rules. The error path goes in the second arg.

## Error messages

- Set `errorMap` once at module scope to localize / standardize messages.
- For form validation, use `z.flatten(error)` and feed to your form library. Don't write custom serializers.

## Performance

- Compile schemas once at module scope. Don't construct them inside request handlers.
- For large unions, prefer discriminated unions (`z.discriminatedUnion("type", [...])`) — they're orders of magnitude faster than plain `z.union`.

## Env vars

- One `env.ts` parses `process.env` at boot:
  ```ts
  const Env = z.object({
    DATABASE_URL: z.string().url(),
    SECRET_KEY: z.string().min(32),
    NODE_ENV: z.enum(["development", "production", "test"]),
  })
  export const env = Env.parse(process.env)
  ```
- Crash on invalid env. Don't run with bad config.

## Don't

- Don't use Zod for runtime checks of internal data after parsing once. The type is the guarantee.
- Don't catch zod errors and rethrow as generic `Error` — you lose the rich `.errors` structure.
- Don't define the type *and* the schema. The schema generates the type.
- Don't validate the same data twice — once at the boundary, once internally. That's a smell that the boundary moved.

Other TypeScript templates