Skip to main content

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')
}
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)
  }
}
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