Appearance
Validation
Input validation is crucial for building secure and reliable APIs. Hono provides built-in validation utilities and integrates seamlessly with popular validation libraries like Zod.
Built-in Validator
Basic Usage
typescript
import { Hono } from 'hono'
import { validator } from 'hono/validator'
const app = new Hono()
app.post('/users',
validator('json', (value, c) => {
const { name, email } = value
if (!name || typeof name !== 'string') {
return c.json({ error: 'Name is required' }, 400)
}
if (!email || !email.includes('@')) {
return c.json({ error: 'Valid email is required' }, 400)
}
// Return validated data
return { name, email }
}),
(c) => {
const data = c.req.valid('json')
return c.json({ created: data }, 201)
}
)Validation Targets
You can validate different parts of the request:
typescript
// JSON body
validator('json', (value, c) => { ... })
// Form data
validator('form', (value, c) => { ... })
// Query parameters
validator('query', (value, c) => { ... })
// Route parameters
validator('param', (value, c) => { ... })
// Headers
validator('header', (value, c) => { ... })
// Cookies
validator('cookie', (value, c) => { ... })Multiple Validators
typescript
app.get('/users/:id',
// Validate route params
validator('param', (value, c) => {
const id = parseInt(value.id)
if (isNaN(id)) {
return c.json({ error: 'Invalid ID' }, 400)
}
return { id }
}),
// Validate query params
validator('query', (value, c) => {
const include = value.include?.split(',') || []
return { include }
}),
(c) => {
const { id } = c.req.valid('param')
const { include } = c.req.valid('query')
return c.json({ id, include })
}
)Zod Integration
Zod is a TypeScript-first schema validation library that works perfectly with Hono.
Installation
bash
npm install zod @hono/zod-validatorBasic Zod Validation
typescript
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
// Define schema
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional()
})
app.post('/users',
zValidator('json', createUserSchema),
(c) => {
const data = c.req.valid('json')
// data is typed as { name: string, email: string, age?: number }
return c.json({ created: data }, 201)
}
)Complex Schemas
typescript
// Nested objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
})
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
address: addressSchema.optional(),
tags: z.array(z.string()).default([])
})
// Union types
const paymentSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('credit_card'),
cardNumber: z.string(),
expiry: z.string()
}),
z.object({
type: z.literal('bank_transfer'),
accountNumber: z.string(),
routingNumber: z.string()
})
])
// Refinements
const passwordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword']
})Validating Query Parameters
typescript
const searchQuerySchema = z.object({
q: z.string().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
sort: z.enum(['asc', 'desc']).default('desc')
})
app.get('/search',
zValidator('query', searchQuerySchema),
(c) => {
const { q, page, limit, sort } = c.req.valid('query')
return c.json({ q, page, limit, sort })
}
)Validating Route Parameters
typescript
const idParamSchema = z.object({
id: z.coerce.number().int().positive()
})
app.get('/users/:id',
zValidator('param', idParamSchema),
(c) => {
const { id } = c.req.valid('param')
return c.json({ userId: id })
}
)Custom Error Handling
typescript
import { zValidator } from '@hono/zod-validator'
const schema = z.object({
email: z.string().email()
})
app.post('/subscribe',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.json({
error: 'Validation failed',
details: result.error.flatten()
}, 400)
}
}),
(c) => {
const { email } = c.req.valid('json')
return c.json({ subscribed: email })
}
)Reusable Validators
typescript
// validators/user.ts
import { z } from 'zod'
export const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
password: z.string().min(8)
})
export const updateUserSchema = createUserSchema.partial()
export const userIdSchema = z.object({
id: z.coerce.number().int().positive()
})
// routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { createUserSchema, updateUserSchema, userIdSchema } from '../validators/user'
const users = new Hono()
users.post('/',
zValidator('json', createUserSchema),
(c) => { /* ... */ }
)
users.put('/:id',
zValidator('param', userIdSchema),
zValidator('json', updateUserSchema),
(c) => { /* ... */ }
)
export default usersOther Validation Libraries
Valibot
bash
npm install valibot @hono/valibot-validatortypescript
import { Hono } from 'hono'
import * as v from 'valibot'
import { vValidator } from '@hono/valibot-validator'
const app = new Hono()
const schema = v.object({
name: v.string([v.minLength(1)]),
email: v.string([v.email()])
})
app.post('/users',
vValidator('json', schema),
(c) => {
const data = c.req.valid('json')
return c.json({ created: data })
}
)TypeBox
bash
npm install @sinclair/typebox @hono/typebox-validatortypescript
import { Hono } from 'hono'
import { Type } from '@sinclair/typebox'
import { tbValidator } from '@hono/typebox-validator'
const app = new Hono()
const schema = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' })
})
app.post('/users',
tbValidator('json', schema),
(c) => {
const data = c.req.valid('json')
return c.json({ created: data })
}
)Custom Validation Middleware
Creating Custom Validators
typescript
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'
interface ValidationError {
field: string
message: string
}
const validateBody = <T>(validate: (body: unknown) => T | ValidationError[]) => {
return createMiddleware(async (c, next) => {
const body = await c.req.json()
const result = validate(body)
if (Array.isArray(result)) {
throw new HTTPException(400, {
message: JSON.stringify({ errors: result })
})
}
c.set('validatedBody', result)
await next()
})
}
// Usage
const validateUser = validateBody((body: any) => {
const errors: ValidationError[] = []
if (!body.name || body.name.length < 1) {
errors.push({ field: 'name', message: 'Name is required' })
}
if (!body.email || !body.email.includes('@')) {
errors.push({ field: 'email', message: 'Valid email required' })
}
if (errors.length > 0) return errors
return { name: body.name, email: body.email }
})
app.post('/users', validateUser, (c) => {
const data = c.get('validatedBody')
return c.json({ created: data })
})File Upload Validation
typescript
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp']
app.post('/upload', async (c) => {
const body = await c.req.parseBody()
const file = body['file'] as File
if (!file) {
return c.json({ error: 'File is required' }, 400)
}
if (file.size > MAX_FILE_SIZE) {
return c.json({ error: 'File too large (max 5MB)' }, 400)
}
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
return c.json({ error: 'Invalid file type' }, 400)
}
return c.json({
filename: file.name,
size: file.size,
type: file.type
})
})Complete Example
typescript
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Schemas
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).max(5).default([]),
published: z.boolean().default(false)
})
const updatePostSchema = createPostSchema.partial()
const postIdSchema = z.object({
id: z.coerce.number().int().positive()
})
const listPostsQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(50).default(10),
tag: z.string().optional(),
published: z.coerce.boolean().optional()
})
// Custom error handler for Zod
const handleZodError = (result: any, c: any) => {
if (!result.success) {
const errors = result.error.issues.map((issue: any) => ({
path: issue.path.join('.'),
message: issue.message
}))
return c.json({
error: 'Validation failed',
details: errors
}, 400)
}
}
// In-memory store
interface Post {
id: number
title: string
content: string
tags: string[]
published: boolean
createdAt: string
updatedAt: string
}
let posts: Post[] = []
let nextId = 1
// Routes
app.get('/posts',
zValidator('query', listPostsQuerySchema, handleZodError),
(c) => {
const { page, limit, tag, published } = c.req.valid('query')
let result = [...posts]
// Filter by tag
if (tag) {
result = result.filter(p => p.tags.includes(tag))
}
// Filter by published status
if (published !== undefined) {
result = result.filter(p => p.published === published)
}
// Paginate
const start = (page - 1) * limit
const paginatedPosts = result.slice(start, start + limit)
return c.json({
posts: paginatedPosts,
pagination: {
page,
limit,
total: result.length,
totalPages: Math.ceil(result.length / limit)
}
})
}
)
app.get('/posts/:id',
zValidator('param', postIdSchema, handleZodError),
(c) => {
const { id } = c.req.valid('param')
const post = posts.find(p => p.id === id)
if (!post) {
throw new HTTPException(404, { message: 'Post not found' })
}
return c.json(post)
}
)
app.post('/posts',
zValidator('json', createPostSchema, handleZodError),
(c) => {
const data = c.req.valid('json')
const now = new Date().toISOString()
const post: Post = {
id: nextId++,
...data,
createdAt: now,
updatedAt: now
}
posts.push(post)
return c.json(post, 201)
}
)
app.put('/posts/:id',
zValidator('param', postIdSchema, handleZodError),
zValidator('json', updatePostSchema, handleZodError),
(c) => {
const { id } = c.req.valid('param')
const data = c.req.valid('json')
const index = posts.findIndex(p => p.id === id)
if (index === -1) {
throw new HTTPException(404, { message: 'Post not found' })
}
posts[index] = {
...posts[index],
...data,
updatedAt: new Date().toISOString()
}
return c.json(posts[index])
}
)
app.delete('/posts/:id',
zValidator('param', postIdSchema, handleZodError),
(c) => {
const { id } = c.req.valid('param')
const index = posts.findIndex(p => p.id === id)
if (index === -1) {
throw new HTTPException(404, { message: 'Post not found' })
}
posts.splice(index, 1)
return c.json({ deleted: true })
}
)
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
export default appSummary
In this chapter, you learned:
- Using Hono's built-in validator
- Validating different request parts (json, query, params, etc.)
- Integrating Zod for schema validation
- Creating complex validation schemas
- Custom error handling
- Building reusable validators
- Using other validation libraries (Valibot, TypeBox)
What's Next?
In the next chapter, we'll explore Testing and learn:
- Unit testing with Hono
- Integration testing
- Testing middleware
- Mocking and fixtures