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