| name | zod |
| description | [Applies to: **/*.{ts,tsx}] This guide outlines definitive best practices for using Zod in TypeScript projects, ensuring robust runtime validation, superior type safety, and maintainable schemas. |
| source | cursor_mdc |
Zod Best Practices
Zod is the definitive TypeScript-first schema validation library. It provides static type inference directly from runtime schemas, catching data-shape errors at both compile time and runtime. Adhere to these guidelines to build resilient, type-safe applications with Zod 4.
Core Setup & Principles
1. Enable Strict TypeScript
Zod's type inference relies on strict: true in your tsconfig.json. This is non-negotiable for reliable type safety.
{
"compilerOptions": {
"strict": true,
}
}
2. Standardize Zod 4 Adoption
Ensure your project and any Zod-dependent libraries use the modern Zod 4 core.
{
"dependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
}
3. Leverage Type Inference (z.infer)
Always infer types from your schemas. This eliminates duplication and keeps types synchronized with validation logic.
import { z } from 'zod';
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
});
interface User {
id: string;
name: string;
email: string;
}
type User = z.infer<typeof userSchema>;
function processUser(user: User) {
console.log(user.email);
}
Data Validation & Error Handling
4. Always Parse Untrusted Data
Never trust external data. Use schema.parse() or schema.safeParse() immediately upon receiving data from APIs, forms, or environment variables.
import { z } from 'zod';
const idSchema = z.string().uuid();
function processId(id: string) {
console.log(id.toUpperCase());
}
function handleRequest(rawId: unknown) {
const result = idSchema.safeParse(rawId);
if (!result.success) {
console.error('Invalid ID received:', result.error.issues);
return { status: 400, message: 'Invalid ID' };
}
processId(result.data);
return { status: 200, message: 'ID processed' };
}
function loadConfig(envVar: unknown) {
{
config = z.({ : z.().() }).(envVar);
.(, config.);
} (error) {
(error z.) {
.(, error.);
process.();
}
error;
}
}
5. Customize Error Messages for UX
Provide clear, user-friendly error messages directly within your schemas.
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email({ message: 'Invalid email address.' }),
password: z.string().min(8, { message: 'Password must be at least 8 characters.' }),
});
Schema Design & Organization
6. Keep Schemas Modular & Domain-Specific
Organize schemas into small, focused files based on their domain. Avoid monolithic schema files.
// ❌ BAD: monolithic schemas.ts
// schemas.ts
export const userSchema = z.object({ /* ... */ });
export const productSchema = z.object({ /* ... */ });
export const orderSchema = z.object({ /* ... */ });
// ✅ GOOD: modular, domain-specific files
// schemas/user.schema.ts
export const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
});
// schemas/product.schema.ts
export const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(3),
price: z.number().positive(),
});
// schemas/index.ts (optional re-export)
export * from './user.schema';
export * from './product.schema';
7. Use Transforms and Refinements for Data Normalization
Normalize and sanitize data as part of validation.
import { z } from 'zod';
const emailInputSchemaBad = z.string().email();
const processEmailBad = (email: string) => email.toLowerCase().trim();
const emailInputSchema = z
.string()
.trim()
.toLowerCase()
.email({ message: 'Invalid email address' });
const postIdSchema = z.string().uuid().transform((val) => `post-${val}`);
const passwordSchema = z.string()
.min(8)
.refine(val => /[A-Z]/.test(val), { message: 'Password must contain an uppercase letter' });
8. Prefer z.coerce for Type Coercion
Use z.coerce for explicit type conversion of known string inputs (e.g., from query parameters, environment variables).
import { z } from 'zod';
const pageParamBad = z.string();
const pageNumberBad = parseInt(pageParamBad.parse('10'));
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
isActive: z.coerce.boolean().default(false),
});
const { page, isActive } = querySchema.parse({ page: '5', isActive: 'true' });
console.log(page, typeof page);
console.log(isActive, typeof isActive);
9. Differentiate optional(), nullable(), and default()
Understand the nuances for robust schema design.
import { z } from 'zod';
const userProfileSchema = z.object({
username: z.string(),
bio: z.string().optional(),
website: z.string().url().nullable(),
status: z.enum(['active', 'inactive']).default('active'),
});
type UserProfile = z.infer<typeof userProfileSchema>;
Testing & Maintenance
10. Write Schema-Focused Unit Tests
Thoroughly test your Zod schemas with both valid and invalid data to prevent regressions.
import { userSchema } from '../../schemas/user.schema';
describe('userSchema', () => {
it('should validate a valid user object', () => {
const validUser = {
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
name: 'John Doe',
email: 'john.doe@example.com',
};
expect(() => userSchema.parse(validUser)).not.toThrow();
});
it('should invalidate an invalid email', () => {
const invalidUser = {
id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef',
name: 'John Doe',
email: 'invalid-email',
};
const result = userSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toBe();
}
});
});