Middlewares 层设计

要点

  • 第 5 篇讲过中间件的基本用法——app.use()await next()c.set() / c.get()。这篇换一个问题切入:当中间件从三五个变成十几个,怎么组织才不会乱
  • Middlewares 层的核心职责是处理「横切关注点」——日志、鉴权、CORS、错误兜底,这些逻辑和具体业务无关,但每个请求都可能用到
  • 全局中间件和路由级中间件要分层挂载。全局层处理所有请求共有的逻辑,路由层处理特定接口的额外约束
  • 中间件之间通过 c.set() / c.get() 传递数据,需要在 Variables 类型里统一定义字段,避免各处写 any
  • 中间件的注册顺序直接决定执行顺序。CORS 要在鉴权之前,错误兜底要包住所有业务逻辑
  • 自定义中间件建议用 createMiddleware() 创建,能拿到完整的类型推导,也能让 c.set() / c.get() 的 key 有编辑器提示
  • 错误处理中间件放在所有中间件和路由的最后面,用 app.onError() 统一兜底,避免每个路由都写 try-catch

1. 从一个文件到一层的演进

第 5 篇的示例里,中间件都是直接写在 index.ts 里的。项目小的时候这种做法够用——三五个中间件,一眼扫完,没什么认知负担。

但当你开始拆分路由模块(第 2 篇讲过),中间件也会跟着膨胀:

  • 认证中间件要拆出「JWT 验证」和「API Key 验证」两个版本
  • 日志中间件想在本地开发时打印更多字段,线上只记关键信息
  • CORS 配置在不同环境有不同的 allowed origins
  • 新加了限流中间件、请求 ID 中间件、权限校验中间件……

这些中间件如果继续散落在 index.ts 和各个路由文件里,会出现几个问题:

  1. 同一个逻辑在多处重复。比如 authMiddleware 在 users 路由和 posts 路由里各写了一份,后续改一处忘了另一处
  2. index.ts 又变回了三四百行,光中间件注册就占了大半
  3. 新成员不知道该去哪里找可用的中间件,只能全局搜

到了这个阶段,把中间件独立成一层(src/middleware/)就是自然的选择。

2. Middlewares 层的职责边界

在进入具体代码之前,先明确 Middlewares 层管什么、不管什么。

管的——请求经过路由 handler 之前或之后,需要统一执行的逻辑:

  • 请求日志、请求计时
  • 身份认证(解析 JWT、验证 API Key)
  • 权限校验(是不是管理员、有没有操作权限)
  • 跨域处理(CORS)
  • 安全响应头
  • 请求 ID 注入(方便链路追踪)
  • 错误兜底(把异常统一转成 JSON 响应)

不管的——某个具体接口的业务逻辑:

  • 参数校验(放在路由 handler 里,或者用单独的 validator)
  • 数据库 CRUD(放在 repositories 层)
  • 业务规则判断(放在 services 层)

一句话概括:Middlewares 层处理的是「所有请求(或某一类请求)都要经过的公共逻辑」,不处理「某个接口特有的业务」。

3. 目录结构:按职责一个文件一个中间件

拆分之后,src/middleware/ 目录大致是这样:

// directory
src/
  middleware/
    auth.ts        认证中间件(JWT 验证)
    api-key.ts     API Key 验证中间件
    logger.ts      请求日志中间件
    error.ts       错误处理(onError 注册)
    request-id.ts  请求 ID 注入
    permissions.ts 权限校验中间件

每个文件导出一个或一组中间件函数。入口文件 index.ts 只负责按顺序挂载,不关心中间件内部的实现细节。

4. 全局中间件 vs 路由级中间件

中间件按作用范围分两级。

全局中间件

挂在 app.use('*', ...) 上,所有请求都会经过。适合放「不管什么接口都要执行」的逻辑:

// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
import type { AppEnv } from './types'
import { requestIdMiddleware } from './middleware/request-id'
import { authMiddleware } from './middleware/auth'
import { registerErrorHandler } from './middleware/error'
 
const app = new Hono<AppEnv>()
 
// 1. 日志——最先注册,确保所有请求(包括被后续中间件拦截的)都有日志
app.use('*', logger())
 
// 2. 请求 ID——在日志之后,这样日志里就能带上请求 ID
app.use('*', requestIdMiddleware())
 
// 3. CORS——在鉴权之前,因为浏览器跨域的 OPTIONS 预检请求不带 token
app.use('*', cors({
  origin: ['https://example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}))
 
// 4. 安全响应头
app.use('*', secureHeaders())
 
// 5. 认证——只有 /api/* 下的接口需要
app.use('/api/*', authMiddleware())
 
// 6. 错误兜底——注册在最外层
registerErrorHandler(app)

推荐的全局中间件注册顺序:

  1. logger — 最先,保证所有请求都被记录
  2. requestId — 给每个请求打一个唯一 ID,后续日志都能带上
  3. cors — 在鉴权之前,否则 OPTIONS 预检会被拦截
  4. secureHeaders — 设置安全响应头
  5. auth — 身份认证(通常只挂在 /api/* 下)
  6. error handler — 通过 app.onError() 注册,兜底所有未捕获的异常

路由级中间件

只对特定路由或路由组生效。有两种挂载方式。

方式一:在子 app 上挂

// src/routes/admin.ts
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { adminOnlyMiddleware } from '../middleware/permissions'
 
export const adminApp = new Hono<AppEnv>()
 
// admin 组下的所有接口都要先过 adminOnly
adminApp.use('*', adminOnlyMiddleware())
 
adminApp.get('/stats', async (c) => {
  // 能走到这里,说明已经是管理员了
  return c.json({ totalUsers: 1024 })
})
 
adminApp.delete('/users/:id', async (c) => {
  const id = c.req.param('id')
  // 删除操作
  return c.json({ message: `User ${id} deleted` })
})

adminApp.use('*', ...) 只对这个子 app 内的路由生效。入口文件里 app.route('/api/admin', adminApp) 挂载后,实际作用范围是 /api/admin/*

方式二:直接写在路由参数里

// src/routes/posts.ts
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { authMiddleware } from '../middleware/auth'
import { ownerOnlyMiddleware } from '../middleware/permissions'
 
export const postsApp = new Hono<AppEnv>()
 
// 获取文章列表——公开接口,不需要认证
postsApp.get('/', async (c) => {
  // ...
  return c.json(posts)
})
 
// 删除文章——需要认证 + 作者本人
postsApp.delete('/:id', authMiddleware(), ownerOnlyMiddleware(), async (c) => {
  const id = c.req.param('id')
  // ...
  return c.json({ message: `Post ${id} deleted` })
})

这种方式适合「只有个别接口需要额外校验」的场景。中间件按从左到右的顺序依次执行,全部通过之后才进入 handler。

5. 认证中间件:把鉴权逻辑独立出来

第 5 篇里写过一个简单的认证中间件,直接把逻辑写在 app.use() 里。到了工程化阶段,把认证逻辑抽成独立文件,方便测试和复用:

// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import type { AppEnv } from '../types'
 
export const authMiddleware = createMiddleware<AppEnv>(async (c, next) => {
  const header = c.req.header('Authorization')
 
  if (!header || !header.startsWith('Bearer ')) {
    return c.json({ error: 'Missing or invalid Authorization header' }, 401)
  }
 
  const token = header.slice(7)
 
  try {
    const secret = c.env.JWT_SECRET
    // 这里省略了具体的 JWT 验证逻辑
    // 假设 verifyJWT 返回解码后的 payload
    const payload = await verifyJWT(token, secret)
 
    // 把用户信息写入 context,后面的路由可以直接取
    c.set('user', {
      id: payload.sub,
      email: payload.email,
      role: payload.role,
    })
 
    await next()
  } catch {
    return c.json({ error: 'Invalid or expired token' }, 401)
  }
})

几个值得注意的点:

  • createMiddleware&lt;AppEnv&gt;() 创建,c.envc.set() / c.get() 都有类型推导
  • JWT 密钥从 c.env.JWT_SECRET 读,不硬编码在代码里
  • 验证通过之后用 c.set('user', ...) 把用户信息存进去,后续路由用 c.get('user') 取出
  • 验证失败直接 return c.json(..., 401),不调用 next(),请求在这里被拦截

verifyJWT 是一个独立的工具函数,放在 src/lib/src/utils/ 里。中间件只负责「从请求里取 token → 调用验证 → 存结果或拦截」这个流程,不关心 JWT 本身的编解码细节。

6. 错误处理中间件:用 onError 统一兜底

如果每个路由 handler 里都写 try-catch 然后返回统一的错误格式,代码会非常冗余:

// anti-pattern.ts
// 不要这样写
app.get('/api/users', async (c) => {
  try {
    const users = await db.select().from(usersTable)
    return c.json(users)
  } catch (error) {
    console.error(error)
    return c.json({ error: 'Internal Server Error' }, 500)
  }
})

Hono 提供了 app.onError() 来统一处理所有未捕获的异常。把它理解成一个「全局 catch 块」:

// src/middleware/error.ts
import type { AppEnv } from '../types'
import type { Hono } from 'hono'
 
// 自定义错误基类,方便区分业务异常和系统异常
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code?: string,
  ) {
    super(message)
  }
}
 
export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(404, `${resource} not found`, 'NOT_FOUND')
  }
}
 
export class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(401, message, 'UNAUTHORIZED')
  }
}
 
// 注册全局错误处理器
export function registerErrorHandler(app: Hono<AppEnv>) {
  app.onError((err, c) => {
    // 业务异常:有明确的状态码和消息
    if (err instanceof AppError) {
      return c.json(
        { error: err.message, code: err.code },
        err.statusCode,
      )
    }
 
    // 系统异常:不暴露具体错误信息给客户端
    console.error(`[Unhandled Error] ${err.message}`, err.stack)
 
    return c.json(
      { error: 'Internal Server Error', code: 'INTERNAL_ERROR' },
      500,
    )
  })
}

路由里只需要抛出自定义错误,不需要自己 catch:

// src/routes/users.ts
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { NotFoundError } from '../middleware/error'
 
export const usersApp = new Hono<AppEnv>()
 
usersApp.get('/:id', async (c) => {
  const id = c.req.param('id')
  const user = await db.select().from(usersTable).where(eq(usersTable.id, id))
 
  if (!user) {
    // 直接抛错,onError 会统一处理响应格式
    throw new NotFoundError('User')
  }
 
  return c.json(user)
})

这样做的好处:

  1. 所有错误响应的格式一致——&#123; error: string, code?: string &#125;
  2. 路由 handler 只关心正常流程,异常处理集中在一个地方
  3. 系统异常的具体堆栈只在服务端日志里打印,不会泄露给客户端

7. 中间件之间怎么传递数据

前面反复用到的 c.set() / c.get(),是中间件之间、中间件与路由之间传递数据的唯一通道。

基本用法

// src/middleware/request-id.ts
import { createMiddleware } from 'hono/factory'
import type { AppEnv } from '../types'
 
export const requestIdMiddleware = createMiddleware<AppEnv>(async (c, next) => {
  // 生成一个请求 ID,优先用客户端传过来的(方便前端追踪)
  const requestId = c.req.header('X-Request-ID') || crypto.randomUUID()
 
  // 写入 context
  c.set('requestId', requestId)
 
  await next()
 
  // 在响应头里也带上,方便客户端排查问题
  c.header('X-Request-ID', requestId)
})

在 Variables 类型里声明字段

c.set() / c.get() 的 key 需要在 AppEnvVariables 里声明,否则 TypeScript 会报错:

// src/types.ts
export type AppEnv = {
  Bindings: {
    JWT_SECRET: string
    DB: D1Database
  }
  Variables: {
    // 每个字段对应一个 c.set() 的 key
    user: {
      id: number
      email: string
      role: string
    }
    requestId: string
    db: DrizzleD1Database
  }
}

声明之后,c.get('user') 会自动推导出 &#123; id: number; email: string; role: string &#125; 类型,c.get('unknown') 则会在编辑阶段就提示错误。

数据流向

一个请求经过多个中间件后,context 里可能积累了这些数据:

// flow
请求进入
  → logger 中间件:(不写入 context,只打印日志)
  → requestId 中间件:c.set('requestId', 'abc-123')
  → auth 中间件:c.set('user', { id: 1, email: '...', role: 'admin' })
  → 路由 handler:c.get('requestId'), c.get('user')

每条数据只在当前请求的生命周期内有效,不同请求的 context 互相隔离。不用担心中间件 A 设置的用户信息会泄露到另一个用户的请求里。

8. 执行顺序:为什么顺序很重要

中间件按注册顺序依次执行,顺序直接影响功能的正确性。第 4 节的代码里已经按推荐顺序排列了,这里解释为什么是这个顺序。

考虑一个反例——如果把鉴权放在 CORS 之前:

// wrong-order.ts
// 鉴权中间件在前,CORS 在后
app.use('/api/*', async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  await next()
})
app.use('/api/*', cors({ origin: 'http://localhost:3000' }))

前端从 http://localhost:3000 跨域发 PUT 请求时,浏览器会先发一个 OPTIONS 预检请求。这个 OPTIONS 请求不带 Authorization 头,直接被鉴权中间件拦截返回 401,真正的 PUT 请求根本不会发出去。

把 CORS 调到鉴权之前,CORS 中间件先处理 OPTIONS 请求并返回正确的响应头,预检通过之后浏览器才发真正的请求。

记住一条原则:越通用的越靠前,越业务相关的越靠后

9. createMiddleware 的好处

前面第 5 节的认证中间件已经用了 createMiddleware&lt;AppEnv&gt;()。这里汇总一下它相比直接写 async (c, next) => &#123; ... &#125; 的好处:

  • c.env 的字段有编辑器补全,不怕拼错环境变量名
  • c.set() 的 key 只能是 Variables 里定义过的,写错了 TypeScript 直接报错
  • c.get() 的返回值有类型推导,不用手动断言
  • 中间件本身有明确的类型标注,作为参数传给 app.use() 时不会有类型冲突

在工程化项目里,建议所有自定义中间件都通过 createMiddleware 创建。写起来多一行 import,换来的是后续维护和重构时的类型安全。

总结

Middlewares 层在 Hono 工程化项目里承担的角色:

  • 把横切关注点(日志、认证、CORS、错误兜底)从业务逻辑里抽离出来,集中放在 src/middleware/ 目录下
  • 全局中间件挂在入口文件上,处理所有请求共有的逻辑;路由级中间件挂在子 app 或单个路由上,处理特定接口的额外约束
  • 中间件之间通过 c.set() / c.get() 传递数据,字段在 Variables 类型里统一定义
  • 注册顺序直接影响正确性:logger → requestId → cors → secureHeaders → auth → error handler
  • createMiddleware&lt;AppEnv&gt;() 创建自定义中间件,拿到完整的类型安全
  • app.onError() 统一处理异常,路由里不需要每个都写 try-catch

下一篇看 Routes 层的设计——路由模块怎么组织、子 app 怎么挂载、路径前缀怎么管理。