路由级中间件
要点
- 全局中间件(
app.use('*', mw))对所有请求生效,路由级中间件只对匹配的路径生效 - Hono 支持在单条路由上直接内联中间件:
app.get('/path', mw1, mw2, handler) - 路由组级中间件通过子 Hono 实例的
subApp.use('*', mw)实现,作用范围跟着app.route()的挂载点走 - 执行顺序是:全局中间件 → 路由组中间件 → 路由级中间件 → handler,返回时反向穿过
- 常见路由级中间件模式包括:认证、限流、请求体大小限制、CORS 差异化配置、分级日志
- 中间件通过
c.set()向 handler 传递数据(用户信息、请求 ID),通过c.get()读取
1. 全局中间件与路由级中间件
03.07 讲过洋葱模型的运行机制。这一篇聚焦中间件的作用范围——哪些请求会经过这个中间件。
全局中间件用 app.use('*') 注册,对所有请求生效。典型用途是日志、CORS、错误处理这类无论什么路径都需要执行的逻辑:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
const app = new Hono()
app.use('*', logger())
app.use('*', cors())
app.get('/health', (c) => c.text('ok'))
app.get('/users', (c) => c.json({ users: [] }))
export default app路由级中间件只在请求路径匹配时才执行。Hono 提供了两种粒度:
- 路径匹配式:
app.use('/admin/*', mw)—— 对匹配路径的所有 HTTP 方法生效 - 路由内联式:
app.get('/admin/users', mw1, mw2, handler)—— 只对这一条路由的指定方法生效
区别在于方法筛选范围。路径匹配式对 GET/POST/PUT/DELETE 都生效,路由内联式只覆盖注册时指定的方法:
// src/index.ts
// /admin/ 下的所有请求都要经过 adminAuth
app.use('/admin/*', adminAuth)
// 只有 GET /login 受 rateLimit 控制,POST /login 不受影响
app.get('/login', rateLimit({ max: 5 }), loginHandler)
app.post('/login', loginHandler)如果写成 app.use('/login', rateLimit()),GET 和 POST 都会被限流。需要按方法区分时,路由内联式更精确。
2. 单路由中间件
路由内联中间件直接写在路由注册方法里,和处理函数并列:
// src/index.ts
import { Hono } from 'hono'
import { authMiddleware } from '@/middleware/auth'
import { validateBody } from '@/middleware/validate'
import { createUserSchema } from '@/schemas/user'
const app = new Hono()
// 单个中间件
app.get('/profile', authMiddleware, (c) => {
const user = c.get('user')
return c.json(user)
})
// 多个中间件——按书写顺序从外向内嵌套
app.post(
'/users',
authMiddleware,
validateBody(createUserSchema),
async (c) => {
const data = c.get('validatedBody')
return c.json({ message: 'created' }, 201)
}
)
export default app多个中间件在一条路由上出现时,按书写顺序嵌套。/users POST 请求进入时依次穿过 authMiddleware → validateBody → handler,返回时反向穿过。这和 03.07 的洋葱模型是同一套机制,只是中间件从 app.use() 独立注册变成了写在路由注册方法内部。
拆分到独立文件
中间件逻辑较复杂时,提到独立文件保持路由注册简洁:
// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
export const authMiddleware = createMiddleware(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Missing token' }, 401)
}
const user = await verifyToken(token)
c.set('user', user)
await next()
})// src/middleware/validate.ts
import type { z } from 'zod'
import { createMiddleware } from 'hono/factory'
export function validateBody<T extends z.ZodType>(schema: T) {
return createMiddleware(async (c, next) => {
try {
const body = await c.req.json()
c.set('validatedBody', schema.parse(body))
await next()
} catch {
return c.json({ error: 'Validation failed' }, 400)
}
})
}createMiddleware 来自 hono/factory,提供类型推导,让 c.set() / c.get() 的键值对在编译期可检查。
3. 路由组中间件
单路由中间件适合「只有这一条路由需要」的场景。一组路由共享相同中间件时,逐条注册重复且容易遗漏。
路由组中间件通过子 Hono 实例实现,04.08 的分层架构已经用过这种模式:
// src/routes/admin/index.ts
import { Hono } from 'hono'
const admin = new Hono()
admin.use('*', authMiddleware) // 只对 admin 实例下的路由生效
admin.use('*', adminRoleMiddleware)
admin.get('/users', (c) => {
return c.json({ users: [], operator: c.get('user') })
})
admin.delete('/users/:id', (c) => c.json({ message: 'deleted' }))
export default admin// src/index.ts
app.use('*', logger()) // 全局:所有请求
app.route('/admin', admin) // admin 实例的中间件:只影响 /admin/*请求 GET /admin/users 的中间件链:logger → authMiddleware → adminRoleMiddleware → handler,返回时反向穿过。admin.use('*', ...) 只对 /admin/* 生效。GET /health 不经过 authMiddleware。
4. 执行顺序
路由级中间件的执行顺序遵循一条规则:从外向内进入,从内向外返回。
「外」和「内」取决于中间件注册的位置:
| 层级 | 注册位置 | 示例 |
|---|---|---|
| 最外层 | 主 app 全局中间件 | app.use('*', logger()) |
| 中间层 | 子实例组中间件 | subApp.use('*', auth) |
| 最内层 | 路由内联中间件 | app.get('/path', mw, handler) |
// src/index.ts
const app = new Hono()
app.use('*', async (c, next) => { // 第 1 层
console.log('1. 全局进入')
await next()
console.log('6. 全局返回')
})
const api = new Hono()
api.use('*', async (c, next) => { // 第 2 层
console.log('2. 组进入')
await next()
console.log('5. 组返回')
})
api.get('/data', async (c, next) => { // 第 3 层
console.log('3. 路由中间件进入')
await next()
console.log('4. 路由中间件返回')
}, (c) => { // handler
console.log('→ handler')
return c.json({ ok: true })
})
app.route('/api', api)
// GET /api/data 输出:1 → 2 → 3 → handler → 4 → 5 → 6顺序对业务逻辑的影响
注册顺序决定中间件在洋葱中的位置,也决定它能看到哪些上游数据:
// 日志中间件需要记录用户信息时,必须注册在认证中间件之后(更内层)
app.use('/api/*', auth()) // 外层:先解析 token,设置 c.set('user', ...)
app.use('*', logger()) // 内层:日志可以看到 c.get('user')
// 反过来则日志拿不到用户信息
app.use('*', logger()) // 外层:此时还没有 user
app.use('/api/*', auth()) // 内层:解析 token如果需要错误处理中间件捕获认证中间件的异常,错误处理就应该注册在认证之前(更外层)。
5. 常见路由级中间件模式
以下是实际项目中高频出现的路由级中间件用法。
认证:只在受保护路由上启用
公开路由不挂载认证中间件,受保护路由才挂载:
// src/routes/index.ts
const app = new Hono()
app.route('/auth', authRoutes) // 登录、注册不需要认证
const protected_ = new Hono().use('*', authMiddleware)
protected_.route('/users', usersRoutes)
protected_.route('/posts', postsRoutes)
app.route('/', protected_)限流:在敏感端点上启用
登录、注册等接口容易被暴力破解,按路由单独配置限流阈值:
// src/routes/auth.ts
const auth = new Hono()
auth.post('/login', rateLimiter({ window: 60, max: 5 }), async (c) => {
const { email, password } = await c.req.json()
return c.json({ token: '...' })
})
auth.post('/register', rateLimiter({ window: 60, max: 3 }), registerHandler)请求体大小限制:上传路由专用
全局限制通常比较小(如 1MB),文件上传需要更大限制但不应该放宽全局设置:
// src/routes/upload.ts
const upload = new Hono()
upload.post('/avatar', bodyParserLimit('5mb'), async (c) => {
const formData = await c.req.formData()
return c.json({ url: '/avatars/xxx.png' })
})
upload.post('/documents', bodyParserLimit('50mb'), async (c) => {
return c.json({ message: 'uploaded' })
})CORS 差异化配置
管理接口比公开接口需要更严格的 CORS 策略。注意同一路径匹配到多个 CORS 中间件时,后注册的会覆盖前面的响应头,避免把全局 CORS 和作用域更小的 CORS 挂在同一路径上:
// src/index.ts
app.use('/api/*', cors()) // 宽松
app.use('/admin/*', cors({ // 严格
origin: ['https://admin.example.com'],
credentials: true,
}))分级日志
开发调试接口输出详细信息,生产接口只记录方法和路径。按路径分别挂载不同详细程度的日志中间件即可:
// src/routes/index.ts
app.use('/api/*', logger()) // 生产:简洁日志
if (env.NODE_ENV !== 'production') {
app.use('/dev/*', debugLogger) // 开发:详细日志
}6. Context 数据传递
中间件和 handler 之间通过 Context 传递数据。03.04 讲过 c.set() / c.get() 的机制。
典型用法:认证中间件解析 token 后存入用户信息,请求追踪中间件生成唯一 ID 存入 Context,下游的 handler、日志、错误处理中间件直接通过 c.get() 读取:
// src/middleware/auth.ts
export const authMiddleware = createMiddleware<Env>(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) return c.json({ error: 'Missing token' }, 401)
const user = await verifyToken(token)
c.set('user', user)
await next()
})// src/middleware/request-id.ts
import { createMiddleware } from 'hono/factory'
import { randomUUID } from 'crypto'
export const requestIdMiddleware = createMiddleware<Env>(async (c, next) => {
const id = c.req.header('x-request-id') ?? randomUUID()
c.set('requestId', id)
c.header('x-request-id', id) // 回传给客户端,方便全链路追踪
await next()
})通过 hono/factory 的泛型参数声明 Context 变量类型,让 c.get('user') 推导出 { id: string; role: 'admin' | 'user' }:
// src/types/context.ts
export type Env = {
Variables: {
user: { id: string; role: 'admin' | 'user' }
requestId: string
}
}中间件和 handler 都声明相同的 Env 类型,c.get() 就能在编译期获得正确的类型。
7. 条件中间件
同一路径下,根据请求特征选择不同的中间件逻辑:
// src/middleware/conditional-auth.ts
import { createMiddleware } from 'hono/factory'
export const conditionalAuth = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') {
await next() // 读操作不需要认证
return
}
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Authentication required' }, 401)
}
await next()
})
app.use('/api/data/*', conditionalAuth)
app.get('/api/data/list', listHandler) // 不需要 token
app.post('/api/data/create', createHandler) // 需要 token条件逻辑简单时,中间件内部 if 判断即可。条件复杂(三种以上分支),建议拆成独立中间件分别挂载到不同路径。按 API 版本分开挂载也是类似思路:app.use('/api/v1/*', authV1) 和 app.use('/api/v2/*', authV2)。
8. 性能考量
路由级中间件的性能影响来自两个方面:中间件自身的计算开销,以及在不需要的路径上白白执行的开销。
每个中间件本质上是一个异步函数。读取 header / Context 的开销极低;JWT 验证(纯 CPU)中等;数据库查询较高;外部 API 调用最高。认证中间件如果需要查数据库,可能消耗 5-50ms。优化方向是缓存验证结果——JWT 本身无状态,验证只需要 CPU 计算;如果必须查数据库,用 Redis 缓存并设置合理 TTL。
中间件数量本身不是瓶颈。5 个只做 header 读写的中间件,比 1 个包含 Redis 查询的中间件更快。
全局中间件在不需要的路径上也会执行。健康检查、静态资源、公开文档不需要认证——04.08 的分层架构已经处理了这个问题,把不需要认证的路由放在独立实例上即可。
9. 测试路由级中间件
Hono 的 app.request() 可以在内存中模拟请求,不需要启动 HTTP 服务器。创建一个只挂载待测中间件的临时 app,验证各种输入条件下的行为:
// tests/middleware/auth.test.ts
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { authMiddleware } from '@/middleware/auth'
describe('authMiddleware', () => {
const app = new Hono()
app.use('*', authMiddleware)
app.get('/test', (c) => c.json({ user: c.get('user') }))
it('无 token 时返回 401', async () => {
const res = await app.request('/test')
expect(res.status).toBe(401)
})
it('有效 token 时放行', async () => {
const token = createTestToken({ id: '1', role: 'user' })
const res = await app.request('/test', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status).toBe(200)
})
it('无效 token 时返回 401', async () => {
const res = await app.request('/test', {
headers: { Authorization: 'Bearer invalid' },
})
expect(res.status).toBe(401)
})
})测试路由组中间件时,把整个子 app 导入即可。app.request('/admin/users', ...) 会自动经过路由组上挂载的所有中间件,验证组合行为。
延伸阅读
- 03.07-中间件洋葱模型 —
await next()的语义和完整流程 - 04.08-大型项目路由组织 — 分层架构中中间件的挂载位置
- 03.04-Context 上下文对象原理 —
c.set()/c.get()的底层机制 - Hono Middleware 文档 — 内置中间件列表和自定义写法
- Hono Factory API —
createMiddleware的类型推导用法
总结
路由级中间件解决的是「哪些请求需要经过哪些处理逻辑」的问题。Hono 提供三个层级的控制粒度:
- 全局中间件:
app.use('*', mw)对所有请求生效,适合日志、CORS、错误处理 - 路由组中间件:子实例的
subApp.use('*', mw)通过app.route()的挂载点限定作用范围,适合按模块配置认证策略 - 路由内联中间件:
app.get('/path', mw1, mw2, handler)精确到单条路由的单个方法,适合限流、校验等特定需求
执行顺序遵循洋葱模型的嵌套规则:全局 → 路由组 → 路由内联 → handler,返回时反向穿过。注册顺序决定了每个中间件在洋葱中的位置,也决定了它能看到哪些上游数据。
中间件通过 c.set() 向下游传递数据,handler 通过 c.get() 读取。用 hono/factory 的 createMiddleware<Env>() 可以获得编译期的类型检查。
下一篇讲 404 处理——当请求没有匹配到任何路由时,Hono 如何生成响应,以及如何自定义 404 行为。