Files
gh-jezweb-claude-skills-ski…/templates/validation-zod.ts
2025-11-30 08:24:59 +08:00

429 lines
11 KiB
TypeScript

/**
* Hono Validation with Zod
*
* Complete examples for request validation using Zod validator.
*/
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// ============================================================================
// BASIC VALIDATION
// ============================================================================
// JSON body validation
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional(),
})
app.post('/users', zValidator('json', userSchema), (c) => {
const data = c.req.valid('json') // Type-safe! { name: string, email: string, age?: number }
return c.json({
success: true,
data,
})
})
// ============================================================================
// QUERY PARAMETER VALIDATION
// ============================================================================
// Search query validation
const searchSchema = z.object({
q: z.string().min(1),
page: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().min(1)),
limit: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int().min(1).max(100))
.optional()
.default('10'),
sort: z.enum(['name', 'date', 'relevance']).optional(),
})
app.get('/search', zValidator('query', searchSchema), (c) => {
const { q, page, limit, sort } = c.req.valid('query')
// All types inferred correctly!
return c.json({
query: q,
page,
limit,
sort,
results: [],
})
})
// ============================================================================
// ROUTE PARAMETER VALIDATION
// ============================================================================
// UUID parameter validation
const uuidParamSchema = z.object({
id: z.string().uuid(),
})
app.get('/users/:id', zValidator('param', uuidParamSchema), (c) => {
const { id } = c.req.valid('param') // Type: string (validated UUID)
return c.json({
userId: id,
name: 'John Doe',
})
})
// Numeric ID parameter
const numericIdSchema = z.object({
id: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
})
app.get('/posts/:id', zValidator('param', numericIdSchema), (c) => {
const { id } = c.req.valid('param') // Type: number
return c.json({
postId: id,
title: 'Post Title',
})
})
// ============================================================================
// HEADER VALIDATION
// ============================================================================
const headerSchema = z.object({
'authorization': z.string().startsWith('Bearer '),
'content-type': z.literal('application/json'),
'x-api-version': z.enum(['1.0', '2.0']).optional(),
})
app.post('/auth', zValidator('header', headerSchema), (c) => {
const headers = c.req.valid('header')
return c.json({
authenticated: true,
apiVersion: headers['x-api-version'] || '1.0',
})
})
// ============================================================================
// FORM DATA VALIDATION
// ============================================================================
const formSchema = z.object({
username: z.string().min(3),
password: z.string().min(8),
remember: z.enum(['on', 'off']).optional(),
})
app.post('/login', zValidator('form', formSchema), (c) => {
const { username, password, remember } = c.req.valid('form')
return c.json({
loggedIn: true,
username,
rememberMe: remember === 'on',
})
})
// ============================================================================
// CUSTOM VALIDATION HOOKS
// ============================================================================
// Custom error response
const customErrorSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
})
app.post(
'/register',
zValidator('json', customErrorSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
issues: result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
})),
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// Throw HTTPException on validation failure
app.post(
'/submit',
zValidator('json', userSchema, (result, c) => {
if (!result.success) {
throw new HTTPException(400, {
message: 'Validation error',
cause: result.error,
})
}
}),
(c) => {
const data = c.req.valid('json')
return c.json({ success: true, data })
}
)
// ============================================================================
// COMPLEX SCHEMA VALIDATION
// ============================================================================
// Nested objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2),
})
const profileSchema = z.object({
name: z.string(),
email: z.string().email(),
address: addressSchema,
tags: z.array(z.string()).min(1).max(5),
})
app.post('/profile', zValidator('json', profileSchema), (c) => {
const data = c.req.valid('json')
// Fully typed nested structure!
return c.json({
success: true,
profile: data,
})
})
// ============================================================================
// CONDITIONAL VALIDATION
// ============================================================================
// Discriminated union
const paymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('card'),
cardNumber: z.string().regex(/^\d{16}$/),
cvv: z.string().regex(/^\d{3}$/),
}),
z.object({
method: z.literal('paypal'),
email: z.string().email(),
}),
z.object({
method: z.literal('bank'),
accountNumber: z.string(),
routingNumber: z.string(),
}),
])
app.post('/payment', zValidator('json', paymentSchema), (c) => {
const payment = c.req.valid('json')
// TypeScript knows the shape based on payment.method
if (payment.method === 'card') {
console.log('Processing card payment:', payment.cardNumber)
}
return c.json({ success: true })
})
// ============================================================================
// REFINEMENTS AND CUSTOM VALIDATION
// ============================================================================
// Custom validation logic
const passwordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
})
app.post('/change-password', zValidator('json', passwordSchema), (c) => {
const data = c.req.valid('json')
return c.json({
success: true,
message: 'Password changed',
})
})
// ============================================================================
// TRANSFORMATION AND COERCION
// ============================================================================
// Transform strings to dates
const eventSchema = z.object({
name: z.string(),
startDate: z.string().transform((val) => new Date(val)),
endDate: z.string().transform((val) => new Date(val)),
capacity: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().int().positive()),
})
app.post('/events', zValidator('json', eventSchema), (c) => {
const event = c.req.valid('json')
// event.startDate is Date, not string!
return c.json({
success: true,
event: {
...event,
startDate: event.startDate.toISOString(),
endDate: event.endDate.toISOString(),
},
})
})
// ============================================================================
// OPTIONAL AND DEFAULT VALUES
// ============================================================================
const settingsSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
notifications: z.boolean().optional(),
language: z.string().default('en'),
maxResults: z.number().int().min(1).max(100).default(10),
})
app.post('/settings', zValidator('json', settingsSchema), (c) => {
const settings = c.req.valid('json')
// Default values applied if not provided
return c.json({
success: true,
settings,
})
})
// ============================================================================
// ARRAY VALIDATION
// ============================================================================
const batchCreateSchema = z.object({
users: z.array(
z.object({
name: z.string(),
email: z.string().email(),
})
).min(1).max(100),
})
app.post('/batch-users', zValidator('json', batchCreateSchema), (c) => {
const { users } = c.req.valid('json')
return c.json({
success: true,
count: users.length,
users,
})
})
// ============================================================================
// MULTIPLE VALIDATORS
// ============================================================================
// Validate multiple targets
const headerAuthSchema = z.object({
authorization: z.string().startsWith('Bearer '),
})
const bodyDataSchema = z.object({
data: z.string(),
})
app.post(
'/protected',
zValidator('header', headerAuthSchema),
zValidator('json', bodyDataSchema),
(c) => {
const headers = c.req.valid('header')
const body = c.req.valid('json')
return c.json({
authenticated: true,
data: body.data,
})
}
)
// ============================================================================
// PARTIAL AND PICK SCHEMAS
// ============================================================================
// Update endpoint - make all fields optional
const updateUserSchema = userSchema.partial()
app.patch('/users/:id', zValidator('json', updateUserSchema), (c) => {
const updates = c.req.valid('json')
// All fields are optional
return c.json({
success: true,
updated: updates,
})
})
// Pick specific fields
const loginSchema = userSchema.pick({ email: true })
app.post('/login', zValidator('json', loginSchema), (c) => {
const { email } = c.req.valid('json')
return c.json({
success: true,
email,
})
})
// ============================================================================
// UNION AND INTERSECTION
// ============================================================================
// Union types (either/or)
const contentSchema = z.union([
z.object({ type: z.literal('text'), content: z.string() }),
z.object({ type: z.literal('image'), url: z.string().url() }),
z.object({ type: z.literal('video'), videoId: z.string() }),
])
app.post('/content', zValidator('json', contentSchema), (c) => {
const content = c.req.valid('json')
return c.json({
success: true,
contentType: content.type,
})
})
// ============================================================================
// EXPORT
// ============================================================================
export default app
// Export schemas for reuse
export {
userSchema,
searchSchema,
uuidParamSchema,
profileSchema,
paymentSchema,
}