Documentation Index Fetch the complete documentation index at: https://mintlify.com/honojs/hono/llms.txt
Use this file to discover all available pages before exploring further.
Custom middleware allows you to extend Hono with your own functionality. This guide shows you how to create middleware for various use cases.
Basic Middleware Structure
A middleware is a function that receives the context object and a next() function:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono ()
const myMiddleware : MiddlewareHandler = async ( c , next ) => {
// Code executed before the route handler
await next ()
// Code executed after the route handler
}
app . use ( myMiddleware )
The Context Object
The context object (c) provides access to the request and response:
import type { MiddlewareHandler } from 'hono'
const exampleMiddleware : MiddlewareHandler = async ( c , next ) => {
// Request properties
const method = c . req . method
const url = c . req . url
const path = c . req . path
const query = c . req . query ( 'key' )
const header = c . req . header ( 'Authorization' )
// Store values in context
c . set ( 'startTime' , Date . now ())
await next ()
// Access stored values
const startTime = c . get ( 'startTime' )
// Modify response
c . res . headers . set ( 'X-Custom-Header' , 'value' )
}
Calling next()
The next() function passes control to the next middleware or route handler. Always await it:
import type { MiddlewareHandler } from 'hono'
const middleware : MiddlewareHandler = async ( c , next ) => {
// ✅ Correct: await next()
await next ()
// ❌ Wrong: not awaiting
// next() // Don't do this!
}
Always await next() to ensure proper middleware execution order and error handling.
Common Middleware Patterns
Before/After Pattern
Execute code before and after the route handler:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono ()
const timingMiddleware : MiddlewareHandler = async ( c , next ) => {
const start = Date . now ()
await next ()
const duration = Date . now () - start
c . res . headers . set ( 'X-Response-Time' , ` ${ duration } ms` )
}
app . use ( timingMiddleware )
app . get ( '/' , ( c ) => c . text ( 'Hello!' ))
Early Return Pattern
Return a response without calling next():
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono ()
const authMiddleware : MiddlewareHandler = async ( c , next ) => {
const token = c . req . header ( 'Authorization' )
if ( ! token ) {
return c . json ({ error: 'Unauthorized' }, 401 )
}
// Token exists, continue to next handler
await next ()
}
app . use ( '/admin/*' , authMiddleware )
app . get ( '/admin/dashboard' , ( c ) => c . text ( 'Dashboard' ))
Modifying Request Data
Add data to the context for later use:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono <{
Variables : {
userId : string
role : string
}
}>()
const userMiddleware : MiddlewareHandler = async ( c , next ) => {
const userId = c . req . header ( 'X-User-Id' ) || 'anonymous'
const role = c . req . header ( 'X-User-Role' ) || 'guest'
c . set ( 'userId' , userId )
c . set ( 'role' , role )
await next ()
}
app . use ( userMiddleware )
app . get ( '/' , ( c ) => {
const userId = c . get ( 'userId' )
const role = c . get ( 'role' )
return c . json ({ userId , role })
})
Modifying Response
Change the response after the handler:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono ()
const jsonWrapperMiddleware : MiddlewareHandler = async ( c , next ) => {
await next ()
// Wrap all JSON responses in a standard format
if ( c . res . headers . get ( 'Content-Type' )?. includes ( 'application/json' )) {
const originalBody = await c . res . json ()
c . res = new Response (
JSON . stringify ({
success: true ,
data: originalBody ,
timestamp: new Date (). toISOString ()
}),
{ headers: c . res . headers }
)
}
}
app . use ( '/api/*' , jsonWrapperMiddleware )
app . get ( '/api/users' , ( c ) => c . json ([{ id: 1 , name: 'Alice' }]))
Parameterized Middleware
Create middleware that accepts options:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
interface RateLimitOptions {
max : number
window : number
}
const rateLimit = ( options : RateLimitOptions ) : MiddlewareHandler => {
const requests = new Map < string , number []>()
return async ( c , next ) => {
const ip = c . req . header ( 'CF-Connecting-IP' ) || 'unknown'
const now = Date . now ()
const windowStart = now - options . window
// Get existing requests for this IP
const ipRequests = requests . get ( ip ) || []
// Filter out old requests
const recentRequests = ipRequests . filter ( time => time > windowStart )
if ( recentRequests . length >= options . max ) {
return c . json (
{ error: 'Rate limit exceeded' },
429
)
}
// Add current request
recentRequests . push ( now )
requests . set ( ip , recentRequests )
await next ()
}
}
const app = new Hono ()
app . use ( '/api/*' , rateLimit ({ max: 10 , window: 60000 })) // 10 requests per minute
app . get ( '/api/data' , ( c ) => c . json ({ data: 'value' }))
Error Handling
Handle errors in middleware:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono ()
const errorHandlerMiddleware : MiddlewareHandler = async ( c , next ) => {
try {
await next ()
} catch ( error ) {
if ( error instanceof HTTPException ) {
// Handle HTTP exceptions
return c . json (
{ error: error . message },
error . status
)
}
// Handle other errors
console . error ( 'Unhandled error:' , error )
return c . json (
{ error: 'Internal Server Error' },
500
)
}
}
app . use ( errorHandlerMiddleware )
app . get ( '/error' , ( c ) => {
throw new HTTPException ( 400 , { message: 'Bad Request' })
})
Async Operations
Perform async operations in middleware:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
const app = new Hono <{
Variables : {
user : { id : string ; email : string } | null
}
}>()
const loadUserMiddleware : MiddlewareHandler = async ( c , next ) => {
const userId = c . req . header ( 'X-User-Id' )
if ( userId ) {
// Simulate async database call
const user = await fetchUserFromDatabase ( userId )
c . set ( 'user' , user )
} else {
c . set ( 'user' , null )
}
await next ()
}
async function fetchUserFromDatabase ( userId : string ) {
// Simulated async database call
return { id: userId , email: `user ${ userId } @example.com` }
}
app . use ( loadUserMiddleware )
app . get ( '/' , ( c ) => {
const user = c . get ( 'user' )
return c . json ({ user })
})
TypeScript Types
Properly type your middleware for better type safety:
import { Hono } from 'hono'
import type { Context , MiddlewareHandler } from 'hono'
// Define your environment type
type Env = {
Variables : {
requestId : string
startTime : number
}
}
// Typed middleware
const requestIdMiddleware : MiddlewareHandler < Env > = async ( c , next ) => {
c . set ( 'requestId' , crypto . randomUUID ())
c . set ( 'startTime' , Date . now ())
await next ()
}
// Or use the Context type directly
const loggingMiddleware = async ( c : Context < Env >, next : () => Promise < void >) => {
const requestId = c . get ( 'requestId' )
const startTime = c . get ( 'startTime' )
console . log ( `[ ${ requestId } ] Request started` )
await next ()
console . log ( `[ ${ requestId } ] Request completed in ${ Date . now () - startTime } ms` )
}
const app = new Hono < Env >()
app . use ( requestIdMiddleware )
app . use ( loggingMiddleware )
app . get ( '/' , ( c ) => {
const requestId = c . get ( 'requestId' ) // TypeScript knows this is a string
return c . json ({ requestId })
})
Factory Pattern
Create a reusable middleware factory:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
interface HeaderOptions {
name : string
value : string | (( c : Context ) => string )
}
function addHeader ( options : HeaderOptions ) : MiddlewareHandler {
return async ( c , next ) => {
await next ()
const value = typeof options . value === 'function'
? options . value ( c )
: options . value
c . res . headers . set ( options . name , value )
}
}
const app = new Hono ()
// Use the factory to create middleware
app . use ( addHeader ({ name: 'X-Powered-By' , value: 'Hono' }))
app . use ( addHeader ({
name: 'X-Request-Path' ,
value : ( c ) => c . req . path
}))
app . get ( '/' , ( c ) => c . text ( 'Hello!' ))
Testing Middleware
Test your custom middleware:
import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'
import { describe , it , expect } from 'vitest'
const authMiddleware : MiddlewareHandler = async ( c , next ) => {
const token = c . req . header ( 'Authorization' )
if ( ! token || token !== 'Bearer valid-token' ) {
return c . json ({ error: 'Unauthorized' }, 401 )
}
await next ()
}
describe ( 'authMiddleware' , () => {
it ( 'should allow valid token' , async () => {
const app = new Hono ()
app . use ( authMiddleware )
app . get ( '/test' , ( c ) => c . json ({ success: true }))
const res = await app . request ( '/test' , {
headers: { Authorization: 'Bearer valid-token' }
})
expect ( res . status ). toBe ( 200 )
expect ( await res . json ()). toEqual ({ success: true })
})
it ( 'should reject invalid token' , async () => {
const app = new Hono ()
app . use ( authMiddleware )
app . get ( '/test' , ( c ) => c . json ({ success: true }))
const res = await app . request ( '/test' , {
headers: { Authorization: 'Bearer invalid-token' }
})
expect ( res . status ). toBe ( 401 )
expect ( await res . json ()). toEqual ({ error: 'Unauthorized' })
})
})
Best Practices
Failing to await next() can cause unexpected behavior and broken error handling: // ❌ Wrong
const bad : MiddlewareHandler = async ( c , next ) => {
console . log ( 'before' )
next () // Missing await!
console . log ( 'after' )
}
// ✅ Correct
const good : MiddlewareHandler = async ( c , next ) => {
console . log ( 'before' )
await next ()
console . log ( 'after' )
}
Don't modify the context object directly
Use c.set() to store values instead of adding properties: // ❌ Wrong
const bad : MiddlewareHandler = async ( c , next ) => {
( c as any ). userId = '123' // Don't do this
await next ()
}
// ✅ Correct
const good : MiddlewareHandler = async ( c , next ) => {
c . set ( 'userId' , '123' )
await next ()
}
Wrap risky operations in try-catch blocks: const middleware : MiddlewareHandler = async ( c , next ) => {
try {
const data = await riskyOperation ()
c . set ( 'data' , data )
await next ()
} catch ( error ) {
console . error ( 'Error:' , error )
return c . json ({ error: 'Operation failed' }, 500 )
}
}
Use TypeScript for better type safety
Define your environment types for better IDE support: type Env = {
Variables : {
userId : string
role : 'admin' | 'user'
}
}
const middleware : MiddlewareHandler < Env > = async ( c , next ) => {
c . set ( 'userId' , '123' )
c . set ( 'role' , 'admin' ) // TypeScript ensures only valid roles
await next ()
}
Each middleware should have a single, clear responsibility: // ✅ Good: Single responsibility
const loggingMiddleware : MiddlewareHandler = async ( c , next ) => {
console . log ( ` ${ c . req . method } ${ c . req . url } ` )
await next ()
}
// ❌ Bad: Too many responsibilities
const kitchenSinkMiddleware : MiddlewareHandler = async ( c , next ) => {
console . log ( 'logging' )
// validate auth
// check rate limits
// transform request
// etc...
await next ()
}
Built-in Middleware Explore built-in middleware for reference
Third-Party Middleware Learn about external middleware packages