我刚开始学习 Express 中间件时,记住的是这句话:
中间件就是一个函数,接收
req、res、next三个参数。
这句话没有错,但它太像定义了。只记住定义,很容易出现一种尴尬:代码会写,却不一定真的知道它为什么存在。等到项目里出现鉴权、日志、跨域、错误处理这些通用逻辑时,才会发现中间件并不是一个语法点,而是一种组织请求流程的方式。
中间件真正重要的地方,不是它长什么样,而是它把 Web 服务里大量横切、重复、可组合的请求处理逻辑,整理成了一条清晰的流水线。
一、如果没有中间件,请求处理会变成什么样?
先看一个最朴素的接口:
1 | app.get('/api/user', async (req, res) => { |
这段代码看起来很干净,因为它只处理“获取用户”这件事。
但真实业务里,一个请求进入服务端之后,通常不会这么简单。你可能还需要:
- 打日志
- 解析请求体
- 处理 Cookie
- 校验登录态
- 校验权限
- 做参数校验
- 设置跨域响应头
- 捕获异常
- 统一返回格式
- 记录接口耗时
如果没有中间件机制,我们很可能会把这些逻辑一股脑塞进每个路由里:
1 | app.get('/api/user', async (req, res) => { |
这样的问题非常明显:
业务逻辑被淹没了
这个接口真正想做的事情只是“根据 id 获取用户”,但前后堆了很多通用逻辑。读代码的人要先穿过一层日志、认证、权限、异常处理,才能看到核心业务。
重复代码会越来越多
登录校验不只
/api/user需要,订单、评论、后台接口都需要。如果每个路由都写一遍,后期改鉴权规则时就会非常痛苦。流程顺序不容易统一
有的接口先校验权限再解析参数,有的接口先查数据库再判断登录,这种差异会让系统行为变得不可预测。
通用能力无法优雅复用
日志、跨域、鉴权、错误处理,本质上不是某一个接口独有的能力,而是很多接口共享的基础设施。
所以,Express 中间件机制首先解决的问题是:
如何把请求处理过程中的通用逻辑,从具体业务路由中抽离出来,并且按顺序组织起来。
二、中间件的本质:把请求处理拆成一条流水线
Express 中间件可以理解成请求流转过程中的一个“处理节点”。
一个请求进来之后,不是直接交给最终路由函数,而是依次经过多个中间件:
1 | 请求进入 |

在 Express 里,这条流水线通常长这样:
1 | app.use(logger) |
每个中间件只负责一件相对独立的事情:
1 | function logger(req, res, next) { |
这里最关键的不是 req、res,而是 next。
next() 的含义是:
当前中间件的工作做完了,请把请求交给下一个处理节点。
如果当前中间件已经完成响应,就不需要再调用 next():
1 | function auth(req, res, next) { |
所以,中间件机制本质上是在提供一种可暂停、可继续、可提前结束的请求处理链。
三、它解决的第一个核心问题:通用逻辑复用
中间件最直接的价值是复用。
比如登录校验:
1 | function requireLogin(req, res, next) { |
你可以把它应用到一组接口:
1 | app.get('/api/profile', requireLogin, getProfile) |
也可以挂载到某个路由前缀上:
1 | app.use('/api/admin', requireLogin) |
这样 /api/admin 下的所有接口都会先经过登录校验。
这就是中间件比“在每个接口里复制一段代码”更优雅的原因:它把重复逻辑变成了可复用、可插拔的模块。
四、它解决的第二个核心问题:关注点分离
一个接口处理函数应该尽量只表达业务意图。
比如下面这个路由:
1 | app.get('/api/user/:id', requireLogin, async (req, res) => { |
读这段代码时,你可以很快抓住重点:
- 这是一个获取用户详情的接口
- 它需要登录
- 它通过
userService.findById查询用户 - 它返回用户数据
至于“如何判断登录”“如何解析 token”“未登录返回什么格式”,这些细节都被放进了 requireLogin。
这就是关注点分离。
路由函数关心业务,中间件关心通用流程。两者配合之后,代码结构会更清楚:
1 | 中间件:我负责请求进入业务之前应该满足什么条件 |
关注点分离还有一个隐性好处:代码更容易测试和维护。
你可以单独测试鉴权中间件,也可以单独测试业务 service,而不需要每次都把所有逻辑混在一个大函数里验证。
五、它解决的第三个核心问题:流程编排
中间件不只是复用函数,它还提供了一种声明式的流程编排方式。
比如一个后台接口可能需要经过这些步骤:
1 | app.post( |
这段代码像一条很清楚的流程说明:
1 | 创建商品之前: |

每一步都是一个中间件:
1 | function requireRole(role) { |
这类“返回中间件的函数”在 Express 里非常常见。它的好处是可以让中间件带参数,从而形成更灵活的组合:
1 | requireRole('admin') |
这说明中间件机制不只是让我们少写代码,它还让我们能像搭积木一样组织请求处理流程。
六、它解决的第四个核心问题:统一错误处理
Express 中还有一种特殊中间件:错误处理中间件。
普通中间件通常接收三个参数:
1 | function middleware(req, res, next) {} |
错误处理中间件接收四个参数:
1 | function errorHandler(err, req, res, next) {} |
它一般放在所有路由的最后:
1 | app.use((err, req, res, next) => { |
当某个中间件或路由把错误交给 next(err) 时,Express 会跳过后续普通中间件,进入错误处理中间件:

1 | app.get('/api/user/:id', async (req, res, next) => { |
这样做的价值很大:
- 每个接口不用都写一套
try...catch返回格式 - 错误日志可以统一记录
- 错误响应结构可以统一
- 业务异常和系统异常可以集中转换
更进一步,我们可以定义业务错误:
1 | class HttpError extends Error { |
然后在错误处理中间件中统一处理:
1 | app.use((err, req, res, next) => { |
于是业务代码可以更自然地表达异常:
1 | if (!user) { |
中间件机制在这里解决的是:
如何让错误从任意请求处理节点冒泡到统一出口。
这和 Promise 链、洋葱模型、责任链模式都有相似之处,本质都是在处理“流程中的异常如何统一收束”。
七、它解决的第五个核心问题:扩展框架能力
Express 本身很轻。
它没有强制内置一整套完整功能,而是把大量能力交给中间件生态来完成。
常见能力几乎都可以通过中间件扩展:
1 | app.use(express.json()) // 解析 JSON 请求体 |
这也是 Express 设计很经典的地方:
框架核心保持简单,请求处理能力通过中间件扩展。
换句话说,中间件机制是 Express 插件化生态的基础。
你不需要等框架官方支持所有功能,只要能力能抽象成“请求进来时做点什么,必要时继续交给下一个节点”,就可以做成中间件。
八、从源码思路理解:中间件像一个队列
为了理解 next(),可以把 Express 中间件机制简化成下面的伪代码:
1 | const middlewares = [] |
真实 Express 源码当然更复杂,它还要处理路由匹配、路径前缀、错误中间件、异步行为等。但核心思想可以先这样理解:
app.use()和app.get()会注册一批处理函数- Express 按注册顺序组织这些函数
- 请求进来后,从第一个匹配的处理函数开始执行
- 当前函数调用
next(),控制权才会交给下一个函数 - 如果直接
res.send()/res.json()并结束响应,请求链就可以停止
所以 next() 不是一个普通回调,它代表的是控制权转移。

这也是为什么中间件顺序非常重要:
1 | app.use(auth) |
和:
1 | app.use('/public', express.static('public')) |
这两段代码的行为可能完全不同。
前者访问静态资源也要先经过鉴权;后者静态资源会先被公开处理。
九、几个容易踩坑的点
1. 忘记调用 next()
1 | app.use((req, res, next) => { |
这个中间件既没有响应客户端,也没有调用 next(),请求就会一直挂起。
正确写法:
1 | app.use((req, res, next) => { |
2. 响应之后又调用 next()
1 | app.use((req, res, next) => { |
这可能导致后续中间件继续尝试写响应,出现类似 Cannot set headers after they are sent 的问题。
一般来说,如果当前中间件已经结束响应,就应该 return:
1 | app.use((req, res, next) => { |
3. 中间件顺序放错
1 | app.post('/api/user', createUser) |
如果 createUser 里需要读取 req.body,上面的顺序就不对。因为 JSON 解析中间件注册在路由后面,请求进入路由时还没有被解析。
正确写法:
1 | app.use(express.json()) |
4. 异步错误没有正确交给错误处理中间件
在异步路由里,如果你只是 throw,不同 Express 版本和写法下可能不会按你预期进入错误处理中间件。更稳妥的方式是显式 next(err),或者使用统一的异步包装函数。
1 | const asyncHandler = fn => { |
十、和 Koa 洋葱模型有什么区别?
Express 的中间件更像线性流水线:
1 | middleware1 -> middleware2 -> middleware3 -> route |
它的思路是:当前节点处理完,调用 next() 进入下一个节点。
Koa 的中间件更典型的是洋葱模型:
1 | app.use(async (ctx, next) => { |
执行顺序是:
1 | before A |

Koa 通过 async/await 让中间件天然具备“进入前”和“返回后”的两段逻辑,非常适合统计耗时、统一包装响应等场景。
Express 也可以做类似事情,比如:
1 | app.use((req, res, next) => { |
但从模型上看,Express 更偏线性责任链,Koa 更强调洋葱式组合。
十一、总结
Express 中间件机制看起来只是 req、res、next,但它背后解决的是 Web 服务非常核心的工程问题:
- 复用:日志、鉴权、跨域、解析请求体等通用逻辑不用在每个接口里重复写
- 分离:业务路由专注业务,中间件处理通用流程
- 编排:多个处理步骤可以按顺序组合成清晰的请求流水线
- 拦截:中间件可以决定继续执行,也可以提前结束响应
- 扩展:Express 的大量能力都可以通过第三方中间件接入
- 兜底:错误处理中间件让异常有统一出口
所以,中间件不是 Express 的一个语法点,而是它组织请求生命周期的核心机制。
真正理解中间件之后,再看 app.use(cors())、app.use(express.json())、app.use(auth),就不会只觉得它们是在“挂函数”,而是在构建一条从请求进入到响应返回的处理链。