Skip to main content

Overview

Middleware are functions that execute during the request-response cycle. They have access to the Context object and can modify the request, response, or pass control to the next middleware.

What is Middleware?

Middleware functions are handlers that:
  • Execute before the final route handler
  • Can perform operations like authentication, logging, or validation
  • Can modify the request or response
  • Must call next() to pass control to the next handler
  • Can short-circuit the chain by returning a response
From src/types.ts:83-88:
type MiddlewareHandler<
  E extends Env = any,
  P extends string = string,
  I extends Input = {},
  R extends HandlerResponse<any> = Response,
> = (c: Context<E, P, I>, next: Next) => Promise<R | void>

Using Middleware

Global Middleware

Apply middleware to all routes:
import { Hono } from 'hono'
import { logger } from 'hono/logger'

const app = new Hono()

// Applies to all routes
app.use('*', logger())

app.get('/', (c) => c.text('Hello'))
app.get('/about', (c) => c.text('About'))

Path-Specific Middleware

Apply middleware to specific paths:
import { cors } from 'hono/cors'

const app = new Hono()

// Only applies to /api/* routes
app.use('/api/*', cors())

app.get('/api/users', (c) => c.json({ users: [] }))
app.get('/public', (c) => c.text('No CORS here'))

Inline Middleware

Use middleware for specific routes:
const authenticate = async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) {
    return c.text('Unauthorized', 401)
  }
  await next()
}

app.get('/protected', authenticate, (c) => {
  return c.text('Protected content')
})

The Next Function

The next function passes control to the next middleware or handler in the chain. From src/types.ts:35:
type Next = () => Promise<void>

Calling Next

const myMiddleware = async (c, next) => {
  console.log('Before handler')
  await next() // Pass control to next handler
  console.log('After handler')
}

app.use('*', myMiddleware)
app.get('/', (c) => {
  console.log('Handler')
  return c.text('Done')
})

// Output:
// Before handler
// Handler
// After handler
Always await the next() call. Without await, the middleware will continue executing before the handler completes, which can lead to unexpected behavior.

Multiple Calls to Next

Calling next() multiple times throws an error:
const badMiddleware = async (c, next) => {
  await next()
  await next() // Error: next() called multiple times
}
From src/compose.ts:33-35:
if (i <= index) {
  throw new Error('next() called multiple times')
}

Middleware Execution Order

Middleware executes in the order it is defined, following the compose pattern from koa-compose. From src/compose.ts:4-73:
export const compose = <E extends Env = Env>(
  middleware: [[Function, unknown], unknown][] | [[Function]][],
  onError?: ErrorHandler<E>,
  onNotFound?: NotFoundHandler<E>
): ((context: Context, next?: Next) => Promise<Context>) => {
  return (context, next) => {
    let index = -1

    return dispatch(0)

    async function dispatch(i: number): Promise<Context> {
      if (i <= index) {
        throw new Error('next() called multiple times')
      }
      index = i

      let res
      let isError = false
      let handler

      if (middleware[i]) {
        handler = middleware[i][0][0]
        context.req.routeIndex = i
      } else {
        handler = (i === middleware.length && next) || undefined
      }

      if (handler) {
        try {
          res = await handler(context, () => dispatch(i + 1))
        } catch (err) {
          if (err instanceof Error && onError) {
            context.error = err
            res = await onError(err, context)
            isError = true
          } else {
            throw err
          }
        }
      } else {
        if (context.finalized === false && onNotFound) {
          res = await onNotFound(context)
        }
      }

      if (res && (context.finalized === false || isError)) {
        context.res = res
      }
      return context
    }
  }
}

Example: Execution Flow

app.use('*', async (c, next) => {
  console.log('Middleware 1 - Before')
  await next()
  console.log('Middleware 1 - After')
})

app.use('*', async (c, next) => {
  console.log('Middleware 2 - Before')
  await next()
  console.log('Middleware 2 - After')
})

app.get('/', (c) => {
  console.log('Handler')
  return c.text('Done')
})

// Output:
// Middleware 1 - Before
// Middleware 2 - Before
// Handler
// Middleware 2 - After
// Middleware 1 - After

Creating Custom Middleware

Basic Middleware

const requestLogger = async (c, next) => {
  const start = Date.now()
  await next()
  const end = Date.now()
  console.log(`${c.req.method} ${c.req.path} - ${end - start}ms`)
}

app.use('*', requestLogger)

Middleware with Configuration

function rateLimiter(options: { max: number; window: number }) {
  const requests = new Map<string, number[]>()

  return async (c, next) => {
    const ip = c.req.header('x-forwarded-for') || 'unknown'
    const now = Date.now()
    const windowStart = now - options.window

    const userRequests = requests.get(ip) || []
    const recentRequests = userRequests.filter(time => time > windowStart)

    if (recentRequests.length >= options.max) {
      return c.text('Too Many Requests', 429)
    }

    recentRequests.push(now)
    requests.set(ip, recentRequests)
    await next()
  }
}

app.use('*', rateLimiter({ max: 100, window: 60000 }))

Type-Safe Middleware

import { MiddlewareHandler } from 'hono'

type AuthEnv = {
  Variables: {
    user: { id: string; name: string }
  }
}

const auth: MiddlewareHandler<AuthEnv> = async (c, next) => {
  const token = c.req.header('Authorization')
  
  if (!token) {
    return c.text('Unauthorized', 401)
  }

  // Validate token and get user
  const user = { id: '123', name: 'John' }
  
  c.set('user', user)
  await next()
}

app.use('*', auth)

app.get('/profile', (c) => {
  const user = c.get('user') // Type-safe!
  return c.json(user)
})

Built-in Middleware

Hono provides many built-in middleware for common tasks:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
import { compress } from 'hono/compress'

const app = new Hono()

app.use('*', logger())
app.use('*', cors())
app.use('/admin/*', basicAuth({ username: 'admin', password: 'secret' }))
app.use('/api/*', bearerAuth({ token: 'your-token' }))

Middleware Registration

From src/hono-base.ts:156-168:
this.use = (arg1: string | MiddlewareHandler<any>, ...handlers: MiddlewareHandler<any>[]) => {
  if (typeof arg1 === 'string') {
    this.#path = arg1
  } else {
    this.#path = '*'
    handlers.unshift(arg1)
  }
  handlers.forEach((handler) => {
    this.#addRoute(METHOD_NAME_ALL, this.#path, handler)
  })
  return this as any
}
The use method:
  • Accepts a path (optional) and one or more handlers
  • Defaults to '*' if no path is provided
  • Registers middleware for all HTTP methods

Short-Circuiting

Middleware can return a response to short-circuit the chain:
const requireAuth = async (c, next) => {
  const token = c.req.header('Authorization')
  
  if (!token) {
    // Short-circuit: don't call next()
    return c.text('Unauthorized', 401)
  }
  
  // Continue to next handler
  await next()
}

app.use('/api/*', requireAuth)

Error Handling in Middleware

Errors thrown in middleware are caught and passed to the error handler:
const riskyMiddleware = async (c, next) => {
  try {
    await someAsyncOperation()
    await next()
  } catch (err) {
    // Handle or re-throw
    throw new Error('Middleware failed')
  }
}

app.use('*', riskyMiddleware)

app.onError((err, c) => {
  console.error(err)
  return c.text('Internal Server Error', 500)
})
From src/compose.ts:50-60, errors are caught during dispatch:
try {
  res = await handler(context, () => dispatch(i + 1))
} catch (err) {
  if (err instanceof Error && onError) {
    context.error = err
    res = await onError(err, context)
    isError = true
  } else {
    throw err
  }
}

Modifying the Response

Middleware can modify the response after the handler executes:
const addHeaders = async (c, next) => {
  await next()
  c.header('X-Custom-Header', 'value')
  c.header('X-Response-Time', Date.now().toString())
}

app.use('*', addHeaders)

Best Practices

  • Always await next() unless intentionally short-circuiting
  • Register global middleware before route definitions
  • Use specific paths for middleware when possible for better performance
  • Keep middleware focused on a single responsibility
  • Use TypeScript to ensure type safety for context variables
Avoid calling next() multiple times in the same middleware. This will throw an error and stop execution.
  • Handlers - Learn about request handlers
  • Context - Access request and response data
  • Validation - Validate request data with middleware

Build docs developers (and LLMs) love