backend-zod
Zod (Schema Validation)
Overview
Zod is a TypeScript-first schema declaration and validation library. Define a schema once, get both runtime validation AND TypeScript types automatically via z.infer<>.
Version: Zod 4 (2025) / Zod 3.x widely used
Requirements: TypeScript ≥5.5, strict mode
Key Benefit: Single source of truth for validation and types — no drift between runtime checks and TypeScript.
When to Use This Skill
✅ Use Zod when:
- Validating API inputs (required for tRPC)
- Building forms with react-hook-form
- Parsing environment variables
- Validating config files or JSON
- Creating DTOs between layers
- Any data crossing trust boundaries
❌ Zod is NOT for:
- Complex business rule validation (use domain logic)
- Database schema definition (use Prisma schema)
- Static type-only definitions (use interfaces)
Quick Start
Installation
npm install zod
Basic Usage
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// Validate
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // typed as User
} else {
console.log(result.error.issues);
}
Core Schema Patterns
Primitives with Validation
const EmailSchema = z.string().email('Invalid email format');
const PasswordSchema = z.string().min(8, 'Min 8 characters');
const AgeSchema = z.number().int().positive().max(150);
const UrlSchema = z.string().url();
const UuidSchema = z.string().uuid();
Object Schemas
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['user', 'admin', 'moderator']),
createdAt: z.date(),
});
type User = z.infer<typeof UserSchema>;
Derive Variations
// For create (omit auto-generated fields)
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
// For update (all optional)
const UpdateUserSchema = CreateUserSchema.partial();
// For public response (only safe fields)
const PublicUserSchema = UserSchema.pick({ id: true, name: true });
Advanced Patterns
Discriminated Unions
Use for type-safe API responses:
const ApiResponse = z.discriminatedUnion('status', [
z.object({ status: z.literal('success'), data: UserSchema }),
z.object({ status: z.literal('error'), code: z.string(), message: z.string() }),
]);
type ApiResponse = z.infer<typeof ApiResponse>;
// TypeScript knows: if status === 'success', data exists
Custom Validation with Refine
const PasswordSchema = z.string()
.min(8)
.refine(val => /[A-Z]/.test(val), 'Must contain uppercase')
.refine(val => /[0-9]/.test(val), 'Must contain number');
Cross-Field Validation with SuperRefine
const FormSchema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: 'custom',
message: 'Passwords must match',
path: ['confirmPassword'],
});
}
});
Transforms (Data Normalization)
// Normalize email
const NormalizedEmail = z.string()
.email()
.transform(s => s.toLowerCase().trim());
// Parse string to number
const StringToNumber = z.string()
.transform(s => parseInt(s, 10))
.pipe(z.number());
Coercion (API Query Parameters)
// GET requests receive strings — use coercion
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
active: z.coerce.boolean().optional(),
});
Common Schema Recipes
Pagination Input
export const PaginationSchema = z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().uuid().optional(),
});
Date Range Filter
export const DateRangeSchema = z.object({
from: z.coerce.date(),
to: z.coerce.date(),
}).refine(d => d.from <= d.to, 'From must be before To');
Environment Variables
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
});
export const env = EnvSchema.parse(process.env);
File Upload Metadata
const FileSchema = z.object({
name: z.string(),
size: z.number().max(10 * 1024 * 1024), // 10MB
type: z.enum(['image/png', 'image/jpeg', 'application/pdf']),
});
Integration with tRPC
import { z } from 'zod';
import { publicProcedure } from '../trpc';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export const userRouter = router({
create: publicProcedure
.input(CreateUserSchema) // ← Zod validates automatically
.mutation(({ input }) => {
// input is typed as { email: string; name: string }
}),
});
Rules
Do ✅
- Use
z.infer<typeof Schema>to derive types - Use discriminated unions over regular unions for objects
- Use
.safeParse()when handling errors gracefully - Add descriptive error messages
- Use
.default()for optional fields with defaults - Keep schemas in dedicated files (
/schemas/*.schema.ts)
Avoid ❌
- Async transforms in tRPC input (not supported)
- Using
.catchall()with output schemas (inference issues) - Multiple Zod installations (causes type inference failures)
- Overly complex nested refinements (hard to debug)
Parse Methods
| Method | Throws | Returns |
|---|---|---|
.parse(data) |
Yes | T |
.safeParse(data) |
No | { success, data?, error? } |
.parseAsync(data) |
Yes | Promise<T> |
.safeParseAsync(data) |
No | Promise<{ success, data?, error? }> |
Use .safeParse() in application code, .parse() in trusted contexts.
Troubleshooting
"Type inference not working":
→ Check single Zod installation: npm ls zod
→ Ensure TypeScript strict mode enabled
→ Restart TypeScript server
"Coercion not working":
→ Use z.coerce.number() not z.number() for query params
→ Check input is string before coercion
"Transform output type wrong":
→ Use .pipe() after transform for additional validation
→ Check transform return type
"Refinement errors unclear":
→ Add path parameter to ctx.addIssue()
→ Use descriptive error messages
File Structure
src/schemas/
├── user.schema.ts # User-related schemas
├── post.schema.ts # Post-related schemas
├── common.schema.ts # Pagination, date ranges, etc.
└── env.schema.ts # Environment validation
References
- https://zod.dev — Official documentation
- https://zod.dev/?id=basic-usage — Quick reference
- https://github.com/colinhacks/zod — GitHub
More from petbrains/mvp-builder
context7
Up-to-date library documentation retrieval using Context7 MCP tools. Process THINK → RESOLVE → FETCH → APPLY. Use when fetching library docs, resolving package names to IDs, getting implementation guides, exploring API references. Provides package resolution strategy, trust score evaluation, token scaling (3K-20K), topic selection patterns.
11frontend-lottie
Decorative JSON animations for UI feedback and polish. Use for loading spinners, success/error checkmarks, empty state illustrations, animated icons. Just plays and loops - no interactivity. For reactive/stateful animations use Rive instead. Lightweight and SSR-compatible.
11backend-pino
High-performance structured JSON logging for Node.js. Use when building production APIs that need fast, structured logs for observability platforms (Datadog, ELK, CloudWatch). Provides request logging middleware, child loggers for context, and sensitive data redaction. Choose Pino over console.log for any production TypeScript backend.
10frontend-color-system
Color palette and theme generation from brand colors. Use when setting up project theming, creating shadcn/Tailwind color schemes, checking WCAG accessibility contrast, or building dark mode. Includes API tools for palette generation and contrast validation.
10