3.3 KiB
3.3 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Avoid Double Validation | HIGH | Parsing the same data twice wastes CPU cycles; in hot paths this adds measurable latency | parse, performance, optimization, architecture |
Avoid Double Validation
Once data is validated by Zod, trust the result. Re-validating the same data in multiple layers doubles CPU usage and adds latency. Pass the typed result through your application instead.
Incorrect (validating at every layer):
import { z } from 'zod'
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
})
// Controller validates
export async function POST(req: NextRequest) {
const body = await req.json()
const user = userSchema.parse(body) // First parse
return await userService.create(user)
}
// Service validates again
const userService = {
async create(data: unknown) {
const user = userSchema.parse(data) // Second parse - redundant
return await userRepository.insert(user)
}
}
// Repository validates again
const userRepository = {
async insert(data: unknown) {
const user = userSchema.parse(data) // Third parse - wasteful
return await db.users.create({ data: user })
}
}
Correct (validate once, pass typed data):
import { z } from 'zod'
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
})
type User = z.infer<typeof userSchema>
// Controller validates at boundary
export async function POST(req: NextRequest) {
const body = await req.json()
const result = userSchema.safeParse(body)
if (!result.success) {
return NextResponse.json({ errors: result.error.issues }, { status: 400 })
}
// Pass validated, typed data
return await userService.create(result.data)
}
// Service receives typed data, no re-validation needed
const userService = {
async create(user: User) {
// user is guaranteed to match schema
return await userRepository.insert(user)
}
}
// Repository receives typed data
const userRepository = {
async insert(user: User) {
return await db.users.create({ data: user })
}
}
When you might validate at multiple layers:
// Different schemas for different layers
const apiUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8), // Only in API layer
})
const dbUserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
passwordHash: z.string(), // Transformed before storage
})
// API validates input format
export async function POST(req: NextRequest) {
const input = apiUserSchema.parse(await req.json())
const user = await userService.create(input)
return NextResponse.json(user)
}
// Service transforms and validates for storage
const userService = {
async create(input: z.infer<typeof apiUserSchema>) {
const dbUser = dbUserSchema.parse({
id: crypto.randomUUID(),
email: input.email,
passwordHash: await hash(input.password),
})
return await userRepository.insert(dbUser)
}
}
When NOT to use this pattern:
- When schemas differ between layers (API vs DB shape)
- When data crosses trust boundaries (external service response)
- During development when debugging data flow
Reference: Zod Performance