Skip to Content
key-conceptsmiddleware

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 └───────────────┘ Response

Each 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: end

The 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 routes

Path-Specific Middleware

Apply to specific paths:

import { cors } from 'hono/cors' app.use('/api/*', cors()) // Only /api/* routes

Method 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 anyway

Avoid 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 routes

Async 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.