Files
gruperly/.agents/skills/zod/references/parse-avoid-double-validation.md
2026-09-04 16:49:24 -03:00

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