Initial commit
This commit is contained in:
428
templates/validation-zod.ts
Normal file
428
templates/validation-zod.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
Reference in New Issue
Block a user