| name | zod-validation |
| description | This skill activates when writing form validation, request validation, or Zod schemas in Hono. It provides patterns for validating JSON bodies, query parameters, path parameters, and headers with proper error handling. |
Zod Validation in Hono
Patterns for request validation using Zod and @hono/zod-validator.
Setup
bun add zod @hono/zod-validator
Basic Validation
JSON Body
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const createUserSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(1, 'Name is required').max(100),
age: z.number().int().positive().optional(),
})
app.post('/users',
zValidator('json', createUserSchema),
async (c) => {
const data = c.req.valid('json')
return c.json(data, 201)
}
)
Query Parameters
const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(['asc', 'desc']).default('desc'),
search: z.string().optional(),
})
app.get('/users',
zValidator('query', paginationSchema),
async (c) => {
const { page, limit, sort, search } = c.req.valid('query')
return c.json({ page, limit, sort, search })
}
)
Path Parameters
const userParamsSchema = z.object({
id: z.string().uuid('Invalid user ID format'),
})
app.get('/users/:id',
zValidator('param', userParamsSchema),
async (c) => {
const { id } = c.req.valid('param')
return c.json({ id })
}
)
Headers
const authHeaderSchema = z.object({
authorization: z.string().startsWith('Bearer '),
'x-request-id': z.string().uuid().optional(),
})
app.get('/protected',
zValidator('header', authHeaderSchema),
async (c) => {
const headers = c.req.valid('header')
return c.json({ authenticated: true })
}
)
Form Data
const uploadSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
})
app.post('/upload',
zValidator('form', uploadSchema),
async (c) => {
const { title, description } = c.req.valid('form')
return c.json({ title, description })
}
)
Schema Patterns
Reusable Field Schemas
export const emailSchema = z.string().email('Invalid email')
export const uuidSchema = z.string().uuid('Invalid ID format')
export const dateSchema = z.coerce.date()
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
Create/Update Pattern
export const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
role: z.enum(['user', 'admin']).default('user'),
})
export const updateUserSchema = createUserSchema.partial()
export const updatePasswordSchema = createUserSchema.pick({
password: true,
}).extend({
currentPassword: z.string(),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
})
export type CreateUser = z.infer<typeof createUserSchema>
export type UpdateUser = z.infer<typeof updateUserSchema>
Nested Objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zip: z.string(),
})
const orderSchema = z.object({
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
})).min(1, 'At least one item required'),
shippingAddress: addressSchema,
billingAddress: addressSchema.optional(),
})
Conditional Validation
const paymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('card'),
cardNumber: z.string().length(16),
cvv: z.string().length(3),
}),
z.object({
method: z.literal('paypal'),
paypalEmail: z.string().email(),
}),
z.object({
method: z.literal('bank'),
accountNumber: z.string(),
routingNumber: z.string(),
}),
])
Custom Refinements
const registrationSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
})
const dateRangeSchema = z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
}).refine(data => data.endDate > data.startDate, {
message: 'End date must be after start date',
path: ['endDate'],
})
Transform
const userInputSchema = z.object({
email: z.string().email().toLowerCase().trim(),
name: z.string().trim(),
tags: z.string().transform(s => s.split(',').map(t => t.trim())),
})
Custom Error Handling
Custom Error Hook
import { zValidator } from '@hono/zod-validator'
const customValidator = <T extends z.ZodType>(
target: 'json' | 'query' | 'param' | 'header' | 'form',
schema: T
) => {
return zValidator(target, schema, (result, c) => {
if (!result.success) {
const errors = result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}))
return c.json({
error: 'Validation failed',
details: errors,
}, 400)
}
})
}
app.post('/users',
customValidator('json', createUserSchema),
async (c) => {
const data = c.req.valid('json')
return c.json(data)
}
)
Validation Error Response Format
{
"error": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email address" },
{ "field": "name", "message": "Name is required" }
]
}
Multiple Validators
Chain validators for different request parts:
app.put('/users/:id',
zValidator('param', userParamsSchema),
zValidator('json', updateUserSchema),
async (c) => {
const { id } = c.req.valid('param')
const data = c.req.valid('json')
return c.json({ id, ...data })
}
)
Type Export Pattern
import { z } from 'zod'
export const createUserSchema = z.object({
email: z.string().email(),
name: z.string(),
})
export const updateUserSchema = createUserSchema.partial()
export const userParamsSchema = z.object({
id: z.string().uuid(),
})
export const userQuerySchema = z.object({
page: z.coerce.number().default(1),
limit: z.coerce.number().default(20),
})
export type CreateUser = z.infer<typeof createUserSchema>
export type UpdateUser = z.infer<typeof updateUserSchema>
export type UserParams = z.infer<typeof userParamsSchema>
export type UserQuery = z.infer<typeof userQuerySchema>
Best Practices
- Always use
zValidator for all request inputs
- Use
z.coerce for query params (they're always strings)
- Provide clear error messages in schema definitions
- Export inferred types for use elsewhere
- Create reusable field schemas for common patterns
- Use
.partial() for update schemas
- Use
.refine() for cross-field validation
- Set sensible defaults with
.default()