Skip to main content
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

Build docs developers (and LLMs) love