Overview
Hono provides built-in validation middleware to validate and transform incoming request data. The validator middleware can validate various parts of the request including JSON body, form data, query parameters, path parameters, headers, and cookies.
The Validator Middleware
The validator middleware is a flexible system for validating request data:
From src/validator/validator.ts:46-88:
export const validator = <
InputType ,
P extends string ,
M extends string ,
U extends ValidationTargetByMethod < M >,
VF extends (
value : unknown extends InputType ? ValidationTargets [ U ] : InputType ,
c : Context < any , P2 >
) => any ,
E extends Env = any ,
>(
target : U ,
validationFunc : VF
): MiddlewareHandler < E , P , V , ExtractValidationResponse <VF>>
Validation Targets
The validator can validate different parts of the request:
json - Request body as JSON
form - Form data (multipart or urlencoded)
query - Query string parameters
param - Path parameters
header - Request headers
cookie - Request cookies
From src/validator/validator.ts:9-12:
type ValidationTargetKeysWithBody = 'form' | 'json'
type ValidationTargetByMethod < M > = M extends 'get' | 'head'
? Exclude < keyof ValidationTargets , ValidationTargetKeysWithBody >
: keyof ValidationTargets
GET and HEAD requests cannot validate json or form targets since these methods must not have a body.
Basic Usage
Validating JSON Body
import { Hono } from 'hono'
import { validator } from 'hono/validator'
const app = new Hono ()
app . post (
'/users' ,
validator ( 'json' , ( value , c ) => {
const parsed = {
name: value . name ,
email: value . email ,
age: parseInt ( value . age )
}
if ( ! parsed . name || ! parsed . email ) {
return c . text ( 'Invalid data' , 400 )
}
return parsed
}),
async ( c ) => {
const { name , email , age } = c . req . valid ( 'json' )
return c . json ({ message: 'User created' , name , email , age })
}
)
Validating Query Parameters
app . get (
'/search' ,
validator ( 'query' , ( value , c ) => {
return {
q: value . q || '' ,
page: parseInt ( value . page ) || 1 ,
limit: parseInt ( value . limit ) || 10
}
}),
( c ) => {
const { q , page , limit } = c . req . valid ( 'query' )
return c . json ({ query: q , page , limit })
}
)
Validating Path Parameters
app . get (
'/users/:id' ,
validator ( 'param' , ( value , c ) => {
const id = parseInt ( value . id )
if ( isNaN ( id ) || id <= 0 ) {
return c . text ( 'Invalid ID' , 400 )
}
return { id }
}),
( c ) => {
const { id } = c . req . valid ( 'param' )
return c . json ({ userId: id })
}
)
app . post (
'/upload' ,
validator ( 'form' , ( value , c ) => {
const file = value . file
const description = value . description
if ( ! file || ! ( file instanceof File )) {
return c . text ( 'File is required' , 400 )
}
return { file , description }
}),
async ( c ) => {
const { file , description } = c . req . valid ( 'form' )
// Process file upload
return c . json ({
message: 'File uploaded' ,
filename: file . name ,
size: file . size
})
}
)
Validation Function
The validation function receives the extracted value and the context:
From src/validator/validator.ts:14-22:
export type ValidationFunction <
InputType ,
OutputType ,
E extends Env = {},
P extends string = string ,
> = (
value : InputType ,
c : Context < E , P >
) => OutputType | TypedResponse | Promise < OutputType > | Promise < TypedResponse >
Synchronous Validation
app . post (
'/data' ,
validator ( 'json' , ( value , c ) => {
// Synchronous validation
if ( ! value . name ) {
return c . text ( 'Name is required' , 400 )
}
return { name: value . name }
}),
( c ) => {
const data = c . req . valid ( 'json' )
return c . json ( data )
}
)
Asynchronous Validation
app . post (
'/users' ,
validator ( 'json' , async ( value , c ) => {
// Async validation - check if email exists
const exists = await checkEmailExists ( value . email )
if ( exists ) {
return c . text ( 'Email already exists' , 409 )
}
return {
email: value . email ,
name: value . name
}
}),
async ( c ) => {
const user = c . req . valid ( 'json' )
// Create user
return c . json ( user )
}
)
Error Handling
Returning Error Responses
Return a Response object to short-circuit the request:
app . post (
'/data' ,
validator ( 'json' , ( value , c ) => {
if ( ! value . email || ! value . email . includes ( '@' )) {
// Return error response - stops execution
return c . json ({ error: 'Invalid email' }, 400 )
}
return value
}),
( c ) => {
// This handler won't execute if validation fails
const data = c . req . valid ( 'json' )
return c . json ({ success: true , data })
}
)
From src/validator/validator.ts:162-170:
const res = await validationFunc ( value as never , c as never )
if ( res instanceof Response ) {
return res as ExtractValidationResponse < VF >
}
c . req . addValidatedData ( target , res as never )
return ( await next ()) as ExtractValidationResponse < VF >
Throwing HTTPException
import { HTTPException } from 'hono/http-exception'
app . post (
'/data' ,
validator ( 'json' , ( value , c ) => {
if ( ! value . name ) {
throw new HTTPException ( 400 , { message: 'Name is required' })
}
return value
}),
( c ) => {
const data = c . req . valid ( 'json' )
return c . json ( data )
}
)
The validator automatically handles malformed data:
From src/validator/validator.ts:94-103:
case 'json' :
if ( ! contentType || ! jsonRegex . test ( contentType )) {
break
}
try {
value = await c . req . json ()
} catch {
const message = 'Malformed JSON in request body'
throw new HTTPException ( 400 , { message })
}
break
Integration with Validation Libraries
Using Zod
import { z } from 'zod'
import { validator } from 'hono/validator'
const userSchema = z . object ({
name: z . string (). min ( 1 ),
email: z . string (). email (),
age: z . number (). int (). positive (). optional ()
})
app . post (
'/users' ,
validator ( 'json' , ( value , c ) => {
const result = userSchema . safeParse ( value )
if ( ! result . success ) {
return c . json ({ error: result . error . flatten () }, 400 )
}
return result . data
}),
( c ) => {
const user = c . req . valid ( 'json' )
// user is typed as { name: string; email: string; age?: number }
return c . json ( user )
}
)
Zod Validator Helper
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const userSchema = z . object ({
name: z . string (). min ( 1 ),
email: z . string (). email (),
age: z . number (). int (). positive (). optional ()
})
app . post (
'/users' ,
zValidator ( 'json' , userSchema ),
( c ) => {
const user = c . req . valid ( 'json' )
// Fully typed based on schema
return c . json ( user )
}
)
Multiple Validators
Chain multiple validators for different targets:
app . post (
'/api/posts/:id/comments' ,
validator ( 'param' , ( value , c ) => {
const id = parseInt ( value . id )
if ( isNaN ( id )) {
return c . text ( 'Invalid post ID' , 400 )
}
return { id }
}),
validator ( 'json' , ( value , c ) => {
if ( ! value . text || value . text . length < 1 ) {
return c . text ( 'Comment text is required' , 400 )
}
return { text: value . text }
}),
async ( c ) => {
const { id } = c . req . valid ( 'param' )
const { text } = c . req . valid ( 'json' )
// Create comment
return c . json ({ postId: id , text })
}
)
Validation Target Implementation
From src/validator/validator.ts:89-160, the validator extracts data based on target:
Query Extraction
Param Extraction
Header Extraction
Cookie Extraction
case 'query' :
value = Object . fromEntries (
Object . entries ( c . req . queries ()). map (([ k , v ]) => {
return v . length === 1 ? [ k , v [ 0 ]] : [ k , v ]
})
)
break
Form data supports both multipart and urlencoded formats:
From src/validator/validator.ts:105-142:
case 'form' : {
if (
! contentType ||
!(multipartRegex.test( contentType ) || urlencodedRegex. test ( contentType ))
) {
break
}
let formData: FormData
if ( c . req . bodyCache . formData ) {
formData = await c . req . bodyCache . formData
} else {
try {
const arrayBuffer = await c . req . arrayBuffer ()
formData = await bufferToFormData ( arrayBuffer , contentType )
c . req . bodyCache . formData = formData
} catch (e) {
let message = 'Malformed FormData request.'
message += e instanceof Error ? ` ${ e . message } ` : ` ${String(e)} `
throw new HTTPException(400, { message })
}
}
const form: BodyData<{ all: true }> = Object.create(null)
formData.forEach((value, key) => {
if (key.endsWith('[]')) {
((form[key] ??= []) as unknown[]).push(value)
} else if (Array.isArray(form[key])) {
(form[key] as unknown[]).push(value)
} else if (Object.hasOwn(form, key)) {
form[key] = [form[key] as string | File, value]
} else {
form[key] = value
}
})
value = form
break
}
app . post (
'/form' ,
validator ( 'form' , ( value , c ) => {
// Fields ending with [] are treated as arrays
return {
tags: value [ 'tags[]' ], // Array of values
name: value . name // Single value
}
}),
( c ) => {
const data = c . req . valid ( 'form' )
return c . json ( data )
}
)
Type Safety
Validators provide full type safety:
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const postSchema = z . object ({
title: z . string (). min ( 1 ). max ( 100 ),
content: z . string (),
published: z . boolean (). default ( false ),
tags: z . array ( z . string ()). optional ()
})
app . post (
'/posts' ,
zValidator ( 'json' , postSchema ),
( c ) => {
const post = c . req . valid ( 'json' )
// post is typed as:
// {
// title: string
// content: string
// published: boolean
// tags?: string[]
// }
return c . json ( post )
}
)
Accessing Validated Data
Access validated data using c.req.valid():
app . post (
'/users' ,
validator ( 'json' , ( value ) => ({
name: value . name ,
email: value . email
})),
( c ) => {
// Access validated JSON data
const user = c . req . valid ( 'json' )
return c . json ( user )
}
)
Custom Validators
Create reusable validator functions:
import { validator } from 'hono/validator'
import type { ValidationFunction } from 'hono/validator'
function emailValidator < P extends string >() {
return validator ( 'json' , ( value , c ) => {
const email = value . email
if ( ! email || typeof email !== 'string' ) {
return c . text ( 'Email is required' , 400 )
}
if ( ! email . includes ( '@' )) {
return c . text ( 'Invalid email format' , 400 )
}
return { email: email . toLowerCase () }
})
}
app . post ( '/register' , emailValidator (), ( c ) => {
const { email } = c . req . valid ( 'json' )
return c . json ({ email })
})
Best Practices
Use validation libraries like Zod for complex schemas
Return specific error messages to help API consumers
Validate and transform data in one step
Use TypeScript to ensure type safety
Chain validators for multiple targets (params, query, body)
Keep validators focused and reusable
GET and HEAD requests cannot validate json or form targets
Always handle validation errors with appropriate status codes
Don’t trust client input - always validate server-side
Validation Patterns
app . get (
'/items' ,
validator ( 'query' , ( value , c ) => {
const page = parseInt ( value . page ) || 1
const limit = parseInt ( value . limit ) || 10
if ( page < 1 ) {
return c . text ( 'Page must be >= 1' , 400 )
}
if ( limit < 1 || limit > 100 ) {
return c . text ( 'Limit must be between 1 and 100' , 400 )
}
return { page , limit }
}),
( c ) => {
const { page , limit } = c . req . valid ( 'query' )
return c . json ({ page , limit })
}
)
Conditional Validation
app . post (
'/users' ,
validator ( 'json' , ( value , c ) => {
const data : any = {
name: value . name ,
email: value . email
}
// Conditional validation
if ( value . age !== undefined ) {
const age = parseInt ( value . age )
if ( isNaN ( age ) || age < 0 ) {
return c . text ( 'Invalid age' , 400 )
}
data . age = age
}
return data
}),
( c ) => {
const user = c . req . valid ( 'json' )
return c . json ( user )
}
)
Handlers - Learn about request handlers
Middleware - Understand middleware execution
Context - Access request and response data