316 lines
8.4 KiB
TypeScript
316 lines
8.4 KiB
TypeScript
/**
|
|
* Hono Validation with Valibot
|
|
*
|
|
* Complete examples for request validation using Valibot validator.
|
|
* Valibot is lighter and faster than Zod, with modular imports.
|
|
*/
|
|
|
|
import { Hono } from 'hono'
|
|
import { vValidator } from '@hono/valibot-validator'
|
|
import * as v from 'valibot'
|
|
import { HTTPException } from 'hono/http-exception'
|
|
|
|
const app = new Hono()
|
|
|
|
// ============================================================================
|
|
// BASIC VALIDATION
|
|
// ============================================================================
|
|
|
|
// JSON body validation
|
|
const userSchema = v.object({
|
|
name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
|
|
email: v.pipe(v.string(), v.email()),
|
|
age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(18))),
|
|
})
|
|
|
|
app.post('/users', vValidator('json', userSchema), (c) => {
|
|
const data = c.req.valid('json') // Type-safe!
|
|
|
|
return c.json({
|
|
success: true,
|
|
data,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// QUERY PARAMETER VALIDATION
|
|
// ============================================================================
|
|
|
|
const searchSchema = v.object({
|
|
q: v.pipe(v.string(), v.minLength(1)),
|
|
page: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
|
|
limit: v.optional(
|
|
v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1), v.maxValue(100)),
|
|
'10'
|
|
),
|
|
sort: v.optional(v.picklist(['name', 'date', 'relevance'])),
|
|
})
|
|
|
|
app.get('/search', vValidator('query', searchSchema), (c) => {
|
|
const { q, page, limit, sort } = c.req.valid('query')
|
|
|
|
return c.json({
|
|
query: q,
|
|
page,
|
|
limit,
|
|
sort,
|
|
results: [],
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ROUTE PARAMETER VALIDATION
|
|
// ============================================================================
|
|
|
|
// UUID parameter
|
|
const uuidParamSchema = v.object({
|
|
id: v.pipe(v.string(), v.uuid()),
|
|
})
|
|
|
|
app.get('/users/:id', vValidator('param', uuidParamSchema), (c) => {
|
|
const { id } = c.req.valid('param')
|
|
|
|
return c.json({
|
|
userId: id,
|
|
name: 'John Doe',
|
|
})
|
|
})
|
|
|
|
// Numeric ID parameter
|
|
const numericIdSchema = v.object({
|
|
id: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
|
|
})
|
|
|
|
app.get('/posts/:id', vValidator('param', numericIdSchema), (c) => {
|
|
const { id } = c.req.valid('param') // Type: number
|
|
|
|
return c.json({
|
|
postId: id,
|
|
title: 'Post Title',
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// HEADER VALIDATION
|
|
// ============================================================================
|
|
|
|
const headerSchema = v.object({
|
|
'authorization': v.pipe(v.string(), v.startsWith('Bearer ')),
|
|
'content-type': v.literal('application/json'),
|
|
'x-api-version': v.optional(v.picklist(['1.0', '2.0'])),
|
|
})
|
|
|
|
app.post('/auth', vValidator('header', headerSchema), (c) => {
|
|
const headers = c.req.valid('header')
|
|
|
|
return c.json({
|
|
authenticated: true,
|
|
apiVersion: headers['x-api-version'] || '1.0',
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// CUSTOM VALIDATION HOOKS
|
|
// ============================================================================
|
|
|
|
const customErrorSchema = v.object({
|
|
email: v.pipe(v.string(), v.email()),
|
|
age: v.pipe(v.number(), v.integer(), v.minValue(18)),
|
|
})
|
|
|
|
app.post(
|
|
'/register',
|
|
vValidator('json', customErrorSchema, (result, c) => {
|
|
if (!result.success) {
|
|
return c.json(
|
|
{
|
|
error: 'Validation failed',
|
|
issues: result.issues,
|
|
},
|
|
400
|
|
)
|
|
}
|
|
}),
|
|
(c) => {
|
|
const data = c.req.valid('json')
|
|
return c.json({ success: true, data })
|
|
}
|
|
)
|
|
|
|
// ============================================================================
|
|
// COMPLEX SCHEMA VALIDATION
|
|
// ============================================================================
|
|
|
|
const addressSchema = v.object({
|
|
street: v.string(),
|
|
city: v.string(),
|
|
zipCode: v.pipe(v.string(), v.regex(/^\d{5}$/)),
|
|
country: v.pipe(v.string(), v.length(2)),
|
|
})
|
|
|
|
const profileSchema = v.object({
|
|
name: v.string(),
|
|
email: v.pipe(v.string(), v.email()),
|
|
address: addressSchema,
|
|
tags: v.pipe(v.array(v.string()), v.minLength(1), v.maxLength(5)),
|
|
})
|
|
|
|
app.post('/profile', vValidator('json', profileSchema), (c) => {
|
|
const data = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
profile: data,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// DISCRIMINATED UNION
|
|
// ============================================================================
|
|
|
|
const paymentSchema = v.variant('method', [
|
|
v.object({
|
|
method: v.literal('card'),
|
|
cardNumber: v.pipe(v.string(), v.regex(/^\d{16}$/)),
|
|
cvv: v.pipe(v.string(), v.regex(/^\d{3}$/)),
|
|
}),
|
|
v.object({
|
|
method: v.literal('paypal'),
|
|
email: v.pipe(v.string(), v.email()),
|
|
}),
|
|
v.object({
|
|
method: v.literal('bank'),
|
|
accountNumber: v.string(),
|
|
routingNumber: v.string(),
|
|
}),
|
|
])
|
|
|
|
app.post('/payment', vValidator('json', paymentSchema), (c) => {
|
|
const payment = c.req.valid('json')
|
|
|
|
if (payment.method === 'card') {
|
|
console.log('Processing card payment:', payment.cardNumber)
|
|
}
|
|
|
|
return c.json({ success: true })
|
|
})
|
|
|
|
// ============================================================================
|
|
// TRANSFORMATIONS
|
|
// ============================================================================
|
|
|
|
const eventSchema = v.object({
|
|
name: v.string(),
|
|
startDate: v.pipe(v.string(), v.transform((val) => new Date(val))),
|
|
endDate: v.pipe(v.string(), v.transform((val) => new Date(val))),
|
|
capacity: v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1)),
|
|
})
|
|
|
|
app.post('/events', vValidator('json', eventSchema), (c) => {
|
|
const event = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
event: {
|
|
...event,
|
|
startDate: event.startDate.toISOString(),
|
|
endDate: event.endDate.toISOString(),
|
|
},
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// OPTIONAL AND DEFAULT VALUES
|
|
// ============================================================================
|
|
|
|
const settingsSchema = v.object({
|
|
theme: v.optional(v.picklist(['light', 'dark']), 'light'),
|
|
notifications: v.optional(v.boolean()),
|
|
language: v.optional(v.string(), 'en'),
|
|
maxResults: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100)), 10),
|
|
})
|
|
|
|
app.post('/settings', vValidator('json', settingsSchema), (c) => {
|
|
const settings = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
settings,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ARRAY VALIDATION
|
|
// ============================================================================
|
|
|
|
const batchCreateSchema = v.object({
|
|
users: v.pipe(
|
|
v.array(
|
|
v.object({
|
|
name: v.string(),
|
|
email: v.pipe(v.string(), v.email()),
|
|
})
|
|
),
|
|
v.minLength(1),
|
|
v.maxLength(100)
|
|
),
|
|
})
|
|
|
|
app.post('/batch-users', vValidator('json', batchCreateSchema), (c) => {
|
|
const { users } = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
count: users.length,
|
|
users,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// PARTIAL SCHEMAS
|
|
// ============================================================================
|
|
|
|
const updateUserSchema = v.partial(userSchema)
|
|
|
|
app.patch('/users/:id', vValidator('json', updateUserSchema), (c) => {
|
|
const updates = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
updated: updates,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNION TYPES
|
|
// ============================================================================
|
|
|
|
const contentSchema = v.variant('type', [
|
|
v.object({ type: v.literal('text'), content: v.string() }),
|
|
v.object({ type: v.literal('image'), url: v.pipe(v.string(), v.url()) }),
|
|
v.object({ type: v.literal('video'), videoId: v.string() }),
|
|
])
|
|
|
|
app.post('/content', vValidator('json', contentSchema), (c) => {
|
|
const content = c.req.valid('json')
|
|
|
|
return c.json({
|
|
success: true,
|
|
contentType: content.type,
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// EXPORT
|
|
// ============================================================================
|
|
|
|
export default app
|
|
|
|
export {
|
|
userSchema,
|
|
searchSchema,
|
|
uuidParamSchema,
|
|
profileSchema,
|
|
paymentSchema,
|
|
}
|