Last Updated: 3/9/2026
Middleware
Middleware is the glue that holds Hono applications together. It processes requests before they reach handlers and responses before they return to clients.
The Onion Model
Think of middleware as layers of an onion:
Request
↓
┌───────────────┐
│ Middleware 1 │ Before
│ ┌─────────┐ │
│ │ Middleware 2 │ Before
│ │ ┌─────┐ │ │
│ │ │ Handler │ │ │ Response
│ │ └─────┘ │ │
│ │ Middleware 2 │ After
│ └─────────┘ │
│ Middleware 1 │ After
└───────────────┘
↓
ResponseEach middleware wraps the next, allowing pre-processing and post-processing.
Handler vs. Middleware
Understanding the difference is crucial:
Handler
A handler returns a Response. Only one handler executes per request.
app.get('/hello', (c) => {
return c.text('Hello!') // Returns Response, stops chain
})Middleware
A middleware calls await next() to continue the chain.
app.use(async (c, next) => {
console.log('Before')
await next() // Continue to next middleware/handler
console.log('After')
})Middleware can also return a Response to short-circuit:
app.use(async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.text('Unauthorized', 401) // Stop here
}
await next() // Continue if authorized
})Execution Order
Middleware executes in registration order:
app.use(async (c, next) => {
console.log('1: start')
await next()
console.log('1: end')
})
app.use(async (c, next) => {
console.log('2: start')
await next()
console.log('2: end')
})
app.get('/', (c) => {
console.log('handler')
return c.text('Hello')
})Output:
1: start
2: start
handler
2: end
1: endThe first registered middleware:
- Executes first before
next() - Executes last after
next()
Applying Middleware
Global Middleware
Apply to all routes:
import { logger } from 'hono/logger'
app.use(logger()) // Applies to all routesPath-Specific Middleware
Apply to specific paths:
import { cors } from 'hono/cors'
app.use('/api/*', cors()) // Only /api/* routesMethod and Path
Apply to specific methods and paths:
import { basicAuth } from 'hono/basic-auth'
app.post('/admin/*', basicAuth({
username: 'admin',
password: 'secret',
}))Multiple Middleware
Chain multiple middleware:
app.use(logger())
app.use('/api/*', cors())
app.post('/admin/*', basicAuth({ /* ... */ }))
app.post('/admin/users', (c) => {
// All three middleware execute before this handler:
// logger() -> cors() -> basicAuth() -> handler
return c.json({ created: true })
})Built-in Middleware
Hono includes powerful middleware out of the box:
Authentication
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret',
}))
// Bearer Token
app.use('/api/*', bearerAuth({ token: 'secret-token' }))
// JWT
app.use('/protected/*', jwt({ secret: 'my-secret' }))Security
import { cors } from 'hono/cors'
import { csrf } from 'hono/csrf'
import { secureHeaders } from 'hono/secure-headers'
app.use(cors())
app.use(csrf())
app.use(secureHeaders())Performance
import { cache } from 'hono/cache'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
app.use(cache({ cacheName: 'my-cache' }))
app.use(compress())
app.use(etag())Utilities
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { requestId } from 'hono/request-id'
app.use(logger())
app.use(prettyJSON())
app.use(requestId())See all built-in middleware in the Reference.
Creating Custom Middleware
Inline Middleware
Simple middleware inline:
app.use(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
})Reusable Middleware
Create reusable middleware with createMiddleware:
import { createMiddleware } from 'hono/factory'
const customLogger = createMiddleware(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
})
app.use(customLogger)Middleware with Options
Create configurable middleware:
import { createMiddleware } from 'hono/factory'
const timing = (options?: { threshold?: number }) => {
return createMiddleware(async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
if (options?.threshold && duration > options.threshold) {
console.warn(`Slow request: ${duration}ms`)
}
c.header('X-Response-Time', `${duration}ms`)
})
}
app.use(timing({ threshold: 1000 }))Type-Safe Middleware
Add type safety with generics:
import { createMiddleware } from 'hono/factory'
type Env = {
Variables: {
user: { id: string; name: string }
}
}
const auth = createMiddleware<Env>(async (c, next) => {
const user = { id: '123', name: 'John' }
c.set('user', user) // Type-safe!
await next()
})
app.use(auth)
app.get('/', (c) => {
const user = c.get('user') // Type is inferred!
return c.json({ user })
})Common Middleware Patterns
Request Timing
app.use(async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
})Request Logging
app.use(async (c, next) => {
console.log(`--> ${c.req.method} ${c.req.path}`)
await next()
console.log(`<-- ${c.res.status}`)
})Error Handling
app.use(async (c, next) => {
try {
await next()
} catch (err) {
console.error('Error:', err)
return c.text('Internal Server Error', 500)
}
})Note: Hono automatically catches errors and passes them to
app.onError(). You rarely need try/catch in middleware.
Authentication
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
// Verify token...
const user = await verifyToken(token)
c.set('user', user)
await next()
})Response Modification
app.use(async (c, next) => {
await next()
// Add header to all responses
c.header('X-Powered-By', 'Hono')
})Context Variables
Share data between middleware and handlers:
app.use(async (c, next) => {
c.set('requestId', crypto.randomUUID())
c.set('startTime', Date.now())
await next()
})
app.get('/', (c) => {
const requestId = c.get('requestId')
const startTime = c.get('startTime')
const duration = Date.now() - startTime
return c.json({ requestId, duration })
})Learn more in Context Object.
Third-Party Middleware
Hono has a rich ecosystem of third-party middleware:
- GraphQL Server —
@hono/graphql-server - Firebase Auth —
@hono/firebase-auth - Sentry —
@hono/sentry - Zod Validator —
@hono/zod-validator - And many more!
Browse the middleware repository .
Error Handling in Middleware
Hono catches all errors automatically:
app.use(async (c, next) => {
await next()
// No need for try/catch!
// Errors are caught and passed to app.onError()
})
app.onError((err, c) => {
console.error('Error:', err)
return c.text('Internal Server Error', 500)
})This means next() will never throw, so there’s no need to wrap it in try/catch/finally.
Performance Considerations
Middleware Order Matters
Place expensive middleware last:
// Good: Cheap middleware first
app.use(requestId()) // Fast
app.use(auth()) // May reject early
app.use(expensiveLogger()) // Only runs if auth passes
// Bad: Expensive middleware first
app.use(expensiveLogger()) // Always runs
app.use(auth()) // May reject anywayAvoid Unnecessary Middleware
Only apply middleware where needed:
// Good: Specific paths
app.use('/api/*', cors())
// Bad: Global when not needed
app.use(cors()) // Applies to all routesAsync Only When Needed
Don’t use async if you don’t need it:
// Good: Sync is faster
app.use((c, next) => {
c.header('X-Custom', 'value')
return next()
})
// Bad: Unnecessary async
app.use(async (c, next) => {
c.header('X-Custom', 'value')
await next()
})What’s Next
Learn about Context: Read Context Object to understand c.set(), c.get(), and more.
Create middleware: Check out the Middleware Guide for advanced patterns.
See all built-in: Browse Built-in Middleware reference.
Understand routing: Learn about Routers and how they work with middleware.