Last Updated: 3/9/2026
Using Middleware
Middleware in Hono processes requests before and after your handlers. Think of it like an onion: the request passes through layers of middleware, reaches your handler, then passes back through on the way out.
How Middleware Works
Middleware wraps your handler like layers of an onion. The request passes through each layer, reaches your handler, then passes back through on the way out.
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
// Layer 1: Logger (outer)
app.use(logger())
// Layer 2: CORS (middle)
app.use(cors())
// Center: Your handler
app.get('/', (c) => c.text('Hello!'))Request flow:
- Request arrives
- Logger middleware (start)
- CORS middleware (start)
- Handler executes
- CORS middleware (end)
- Logger middleware (end)
- Response sent
Built-in Middleware
Hono includes powerful middleware out of the box.
Logger
Log every request:
import { logger } from 'hono/logger'
app.use(logger())
app.get('/', (c) => c.text('Hello'))<-- GET /
--> GET / 200 3msCORS
Enable Cross-Origin Resource Sharing:
import { cors } from 'hono/cors'
// Simple CORS - allows all origins
app.use(cors())
// Configured CORS
app.use(cors({
origin: 'https://example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 600,
credentials: true,
}))Basic Authentication
Protect routes with username/password:
import { basicAuth } from 'hono/basic-auth'
app.use('/admin/*',
basicAuth({
username: 'admin',
password: 'secret',
})
)
app.get('/admin/dashboard', (c) => {
return c.text('Admin Dashboard')
})Bearer Authentication
Protect routes with bearer tokens:
import { bearerAuth } from 'hono/bearer-auth'
app.use('/api/*',
bearerAuth({ token: 'your-secret-token' })
)With dynamic token verification:
app.use('/api/*',
bearerAuth({
verifyToken: async (token, c) => {
return token === await getValidToken()
},
})
)JWT Authentication
Validate JSON Web Tokens:
import { jwt } from 'hono/jwt'
app.use('/api/*',
jwt({
secret: 'my-secret-key',
})
)
app.get('/api/protected', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload.sub })
})With custom claims:
app.use('/api/*',
jwt({
secret: 'my-secret-key',
})
)
app.get('/api/me', (c) => {
const payload = c.get('jwtPayload')
return c.json({
userId: payload.sub,
email: payload.email,
role: payload.role,
})
})Compression
Compress responses:
import { compress } from 'hono/compress'
app.use(compress())
// Or specify encoding
app.use(compress({ encoding: 'gzip' }))ETag
Generate ETags for caching:
import { etag } from 'hono/etag'
app.use(etag())
app.get('/data', (c) => {
return c.json({ data: 'This will have an ETag' })
})Pretty JSON
Format JSON responses:
import { prettyJSON } from 'hono/pretty-json'
app.use(prettyJSON())
app.get('/json', (c) => {
return c.json({ message: 'This will be pretty-printed' })
})Secure Headers
Add security headers:
import { secureHeaders } from 'hono/secure-headers'
app.use(secureHeaders())
// Or customize
app.use(secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
},
xFrameOptions: 'DENY',
}))Request ID
Add unique IDs to requests:
import { requestId } from 'hono/request-id'
app.use(requestId())
app.get('/', (c) => {
const id = c.get('requestId')
return c.json({ requestId: id })
})Timeout
Limit request execution time:
import { timeout } from 'hono/timeout'
app.use('/api/*', timeout(5000)) // 5 seconds
app.get('/api/slow', async (c) => {
await longRunningTask() // Will timeout after 5s
return c.json({ done: true })
})Body Limit
Limit request body size:
import { bodyLimit } from 'hono/body-limit'
app.use('/upload/*',
bodyLimit({
maxSize: 50 * 1024, // 50kb
onError: (c) => {
return c.text('File too large', 413)
},
})
)Applying Middleware
Global Middleware
Apply to all routes:
import { logger } from 'hono/logger'
app.use(logger()) // Applies to all routes
app.get('/a', (c) => c.text('A')) // Logged
app.get('/b', (c) => c.text('B')) // LoggedPath-Specific Middleware
Apply to specific paths:
import { basicAuth } from 'hono/basic-auth'
// Only /admin/* routes
app.use('/admin/*', basicAuth({ username: 'admin', password: 'secret' }))
app.get('/admin/users', (c) => c.text('Users')) // Protected
app.get('/public/info', (c) => c.text('Info')) // Not protectedMultiple Paths
Apply to multiple paths:
import { cors } from 'hono/cors'
app.use(['/api/*', '/webhook/*'], cors())
app.get('/api/data', (c) => c.json({})) // CORS enabled
app.get('/webhook/event', (c) => c.json({})) // CORS enabled
app.get('/public', (c) => c.text('Public')) // No CORSPer-Route Middleware
Apply to a single route:
import { cache } from 'hono/cache'
app.get('/cached',
cache({ cacheName: 'my-cache' }),
(c) => c.json({ data: 'Cached response' })
)
app.get('/not-cached', (c) => c.json({ data: 'Not cached' }))Multiple Middleware
Chain multiple middleware:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'
app.get('/protected',
logger(),
cors(),
basicAuth({ username: 'admin', password: 'secret' }),
(c) => c.text('Protected content')
)Custom Middleware
Basic Custom Middleware
app.use(async (c, next) => {
console.log('Before handler')
await next() // Call the next middleware/handler
console.log('After handler')
})Timing Middleware
Measure request duration:
app.use(async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
})Error Handling Middleware
Catch and log errors:
app.use(async (c, next) => {
try {
await next()
} catch (err) {
console.error('Error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
}
})Request Logging Middleware
Log request details:
app.use(async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`)
console.log('Headers:', c.req.header())
await next()
console.log('Status:', c.res.status)
})Authentication Middleware
Custom auth logic:
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
const user = await verifyToken(token)
if (!user) {
return c.json({ error: 'Invalid token' }, 401)
}
c.set('user', user)
await next()
})
app.get('/api/me', (c) => {
const user = c.get('user')
return c.json({ user })
})Creating Reusable Middleware
Factory Pattern
import { createMiddleware } from 'hono/factory'
const timing = createMiddleware(async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
})
app.use(timing)With Options
import { createMiddleware } from 'hono/factory'
interface RateLimitOptions {
max: number
window: number
}
const rateLimit = (options: RateLimitOptions) => {
const requests = new Map<string, number[]>()
return createMiddleware(async (c, next) => {
const ip = c.req.header('CF-Connecting-IP') || 'unknown'
const now = Date.now()
const userRequests = requests.get(ip) || []
const recentRequests = userRequests.filter(
(time) => now - time < options.window
)
if (recentRequests.length >= options.max) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
recentRequests.push(now)
requests.set(ip, recentRequests)
await next()
})
}
// Use it
app.use('/api/*', rateLimit({ max: 100, window: 60000 })) // 100 req/minType-Safe Middleware
import { createMiddleware } from 'hono/factory'
type Env = {
Variables: {
user: { id: number; name: string }
}
}
const auth = createMiddleware<Env>(async (c, next) => {
const user = await authenticate(c.req.header('Authorization'))
c.set('user', user) // Type-safe!
await next()
})Middleware Execution Order
Order matters! Middleware executes in registration order:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'
// 1. Logger runs first
app.use(logger())
// 2. Then CORS
app.use(cors())
// 3. Then auth
app.use('/admin/*', basicAuth({ username: 'admin', password: 'secret' }))
// 4. Finally, the handler
app.get('/admin/dashboard', (c) => c.text('Dashboard'))Request flow:
Request
→ Logger (start)
→ CORS (start)
→ BasicAuth (start)
→ Handler
← BasicAuth (end)
← CORS (end)
← Logger (end)
ResponseCommon pattern:
// 1. Logging/monitoring
app.use(logger())
// 2. Security headers
app.use(secureHeaders())
// 3. CORS
app.use('/api/*', cors())
// 4. Authentication
app.use('/api/*', jwt({ secret: 'secret' }))
// 5. Request validation
app.use('/api/*', bodyLimit({ maxSize: 1024 * 1024 }))
// 6. Your routes
app.get('/api/data', handler)Third-Party Middleware
The community provides additional middleware:
GraphQL Server
import { graphqlServer } from '@hono/graphql-server'
app.use('/graphql', graphqlServer({
schema: mySchema,
}))Firebase Auth
import { firebaseAuth } from '@hono/firebase-auth'
app.use('/api/*',
firebaseAuth({
projectId: 'your-project-id',
})
)Sentry
import { sentry } from '@hono/sentry'
app.use(sentry({ dsn: 'your-sentry-dsn' }))Common Patterns
Conditional Middleware
Apply middleware based on conditions:
app.use(async (c, next) => {
if (c.req.header('X-Debug') === 'true') {
console.log('Debug mode enabled')
}
await next()
})Middleware Composition
Combine multiple middleware:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { compress } from 'hono/compress'
const apiMiddleware = [
logger(),
cors(),
compress(),
]
app.use('/api/*', ...apiMiddleware)Early Returns
Stop execution in middleware:
app.use('/api/*', async (c, next) => {
if (!c.req.header('X-API-Key')) {
return c.json({ error: 'API key required' }, 401)
// next() is NOT called - execution stops
}
await next()
})Response Modification
Modify responses after handlers:
app.use(async (c, next) => {
await next()
// Add header to all responses
c.res.headers.set('X-Powered-By', 'Hono')
// Modify JSON responses
if (c.res.headers.get('Content-Type')?.includes('application/json')) {
const body = await c.res.json()
return c.json({
...body,
timestamp: new Date().toISOString(),
})
}
})What’s Next?
- Learn JSX: JSX Templating
- Type-safe APIs: RPC Client
- Middleware reference: Built-in Middleware