Express 基础到中间件:系统掌握常用 API 与请求处理链
- 前言
- [1. Express 基础:先建立运行模型](#1. Express 基础:先建立运行模型)
-
- [1.1 Express 是什么,`express()` 又创建了什么](#1.1 Express 是什么,
express()又创建了什么) - [1.2 `express.json()` 与 `app.listen()` 各自负责什么](#1.2
express.json()与app.listen()各自负责什么)
- [1.1 Express 是什么,`express()` 又创建了什么](#1.1 Express 是什么,
- [2. Express 路由:让 HTTP 方法找到业务处理逻辑](#2. Express 路由:让 HTTP 方法找到业务处理逻辑)
-
- [2.1 路由由请求方法、路径和处理函数共同组成](#2.1 路由由请求方法、路径和处理函数共同组成)
- [2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE](#2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE)
- [3. Request 请求对象:数据究竟从哪里来](#3. Request 请求对象:数据究竟从哪里来)
-
- [3.1 `req.body`、`req.query`、`req.params` 与 `req.headers`](#3.1
req.body、req.query、req.params与req.headers) - [3.2 三个容易踩中的请求数据误区](#3.2 三个容易踩中的请求数据误区)
- [3.1 `req.body`、`req.query`、`req.params` 与 `req.headers`](#3.1
- [4. Response 响应对象:状态码与数据如何返回前端](#4. Response 响应对象:状态码与数据如何返回前端)
-
- [4.1 `res` 对象与常用响应方法](#4.1
res对象与常用响应方法) - [4.2 状态码不是装饰,而是响应语义的一部分](#4.2 状态码不是装饰,而是响应语义的一部分)
- [4.1 `res` 对象与常用响应方法](#4.1
- [5. Express 中间件:一条按顺序推进的请求处理链](#5. Express 中间件:一条按顺序推进的请求处理链)
-
- [5.1 什么是 Middleware,`app.use()` 做了什么](#5.1 什么是 Middleware,
app.use()做了什么) - [5.2 `next()` 为什么能进入下一层,执行顺序从哪里来](#5.2
next()为什么能进入下一层,执行顺序从哪里来) - [5.3 `app.use(express.json())` 为什么仍然是在注册中间件](#5.3
app.use(express.json())为什么仍然是在注册中间件) - [5.4 自定义中间件与路由到底是什么关系](#5.4 自定义中间件与路由到底是什么关系)
- [5.1 什么是 Middleware,`app.use()` 做了什么](#5.1 什么是 Middleware,
- [6. 错误处理中间件:让异常沿另一条链统一收口](#6. 错误处理中间件:让异常沿另一条链统一收口)
-
- [6.1 `next()` 与 `next(error)` 的本质区别](#6.1
next()与next(error)的本质区别) - [6.2 从路由异常到统一错误响应的完整过程](#6.2 从路由异常到统一错误响应的完整过程)
- [6.3 为什么错误处理中间件通常放在路由后面](#6.3 为什么错误处理中间件通常放在路由后面)
- [6.1 `next()` 与 `next(error)` 的本质区别](#6.1
- [7. 串起 Express 的完整请求生命周期](#7. 串起 Express 的完整请求生命周期)
-
- [7.1 一次请求如何穿过普通链与错误链](#7.1 一次请求如何穿过普通链与错误链)
- [7.2 用一套简化示例完成最终复盘](#7.2 用一套简化示例完成最终复盘)
- 总结
前言
刚接触 Express 时,很多人会把它理解成一组 API:用 app.get() 写查询接口,用 app.post() 接收数据,再用 res.json() 返回结果。这样确实能把服务跑起来,但一旦加入登录校验、请求日志、参数解析和统一错误处理,代码很容易变得混乱。
真正理解 Express 的关键,不是背诵方法名,而是建立一幅清晰的运行图景:一次 HTTP 请求进入应用后,会沿着注册顺序依次经过若干处理函数;每一层都可以读取请求、修改上下文、结束响应,或者把控制权交给下一层。 这条按顺序执行的处理链,就是 Express 最核心的设计。
本文先梳理创建应用、路由、请求对象和响应对象,再重点拆解普通中间件与错误处理中间件。读完后,不仅能写出常见的增删改查接口,也能解释 app.use(express.json()) 为什么有效、next() 究竟做了什么,以及异常如何从路由一路流向统一错误出口。
1. Express 基础:先建立运行模型
1.1 Express 是什么,express() 又创建了什么
Express 是运行在 Node.js 之上的 Web 框架。Node.js 原生 http 模块已经能接收请求、发送响应,但开发者还要自行判断 URL、区分请求方法、解析请求体,并组织大量重复逻辑。Express 在这些底层能力之上提供了更简洁的路由系统和中间件机制,让开发者把精力集中在业务处理上。
Express 应用可以看成一个由路由和中间件组成的请求处理器。 它接收 Node.js 的请求与响应对象,对其进行增强,然后按照注册顺序调度处理函数。
安装 Express 后,可以用最少的代码启动一个 HTTP 服务:
bash
npm install express
javascript
const express = require('express');
const app = express();
app.get('/hello', (req, res) => {
res.json({ message: 'Hello Express' });
});
app.listen(3000, '127.0.0.1', () => {
console.log('Server is running at http://127.0.0.1:3000');
});
express() 是一个工厂函数,调用后返回应用对象 app。后续的 app.use()、app.get()、app.post() 和 app.listen(),本质上都在配置或启动这个应用。上面的请求到达后,Express 会寻找与 GET /hello 匹配的处理函数,并通过 res.json() 结束本次请求---响应周期。
1.2 express.json() 与 app.listen() 各自负责什么
客户端发送 JSON 时,网络上传输的是一段字节流,Express 不会凭空知道它对应哪个 JavaScript 对象。express.json() 提供 JSON 请求体解析能力,常见注册方式如下:
javascript
app.use(express.json());
app.post('/users', (req, res) => {
console.log(req.body);
res.status(201).json({ data: req.body });
});
当请求头包含合适的 Content-Type: application/json 时,解析器会读取请求体、尝试解析 JSON,并把结果放到 req.body。因此它通常要注册在需要读取 req.body 的路由之前。解析失败时,错误会进入 Express 的错误处理流程。
app.listen() 则让应用开始监听网络连接。常见参数如下表所示:
| 参数 | 作用 | 常见取值 |
|---|---|---|
port |
指定监听端口 | 3000、8080 |
host |
指定监听地址 | 127.0.0.1、0.0.0.0 |
callback |
监听成功后执行 | 输出启动信息、执行初始化提示 |
127.0.0.1 只允许本机访问,适合本地开发;0.0.0.0 表示监听本机所有 IPv4 网络接口,常用于容器或局域网访问。只写 app.listen(3000) 时,底层会使用系统默认的未指定地址,具体表现可能受操作系统网络栈影响。开发阶段显式写出 host,更容易判断服务究竟对谁开放。
2. Express 路由:让 HTTP 方法找到业务处理逻辑
2.1 路由由请求方法、路径和处理函数共同组成
HTTP 请求不只有 URL,还包含请求方法。Express 用 app.get()、app.post()、app.put()、app.patch() 和 app.delete() 对应常见 HTTP 方法。它们的基本形式都是:
javascript
app.METHOD(path, handler);
只有请求方法与路径都匹配 ,对应的处理函数才会进入执行流程。例如 app.get('/users') 不会处理 POST /users,app.post('/users') 也不会处理 POST /articles。
| Express API | HTTP 方法 | 常见语义 | 典型结果 |
|---|---|---|---|
app.get() |
GET | 查询资源 | 返回列表或详情 |
app.post() |
POST | 创建资源 | 创建成功后返回新资源 |
app.put() |
PUT | 整体替换资源 | 用完整数据替换旧状态 |
app.patch() |
PATCH | 局部更新资源 | 只修改提交的字段 |
app.delete() |
DELETE | 删除资源 | 返回删除结果或空响应 |
这些语义是 API 设计约定,不是 Express 自动实现的业务规则。比如调用 app.delete() 不会自动删除数据库记录,它只是匹配 DELETE 请求;真正的删除动作仍由处理函数完成。
2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE
下面用内存数组演示五种常见路由。示例重点是观察 HTTP 方法与业务意图的对应关系:
javascript
const users = [{ id: 1, name: 'Ada', role: 'admin' }];
app.get('/users', (req, res) => {
res.json({ data: users });
});
app.post('/users', (req, res) => {
const user = { id: Date.now(), ...req.body };
users.push(user);
res.status(201).json({ data: user });
});
app.put('/users/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((user) => user.id === id);
users[index] = { id, name: req.body.name, role: req.body.role };
res.json({ data: users[index] });
});
app.patch('/users/:id', (req, res) => {
const id = Number(req.params.id);
const user = users.find((item) => item.id === id);
Object.assign(user, req.body);
res.json({ data: user });
});
app.delete('/users/:id', (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((user) => user.id === id);
const [removedUser] = users.splice(index, 1);
res.json({ data: removedUser });
});
PUT 与 PATCH 最容易混淆。可以把 PUT 理解为"提交一份完整的新状态",未提交字段可能不再保留;PATCH 则表示"只提交需要修改的部分"。实际项目还需要处理参数校验、资源不存在、ID 非法等情况,这些问题会在后面的错误处理链中统一解决。
3. Request 请求对象:数据究竟从哪里来
3.1 req.body、req.query、req.params 与 req.headers
路由处理函数中的 req 表示当前请求。请求数据可能位于路径、查询字符串、请求体或请求头中,位置不同,表达的含义也不同。假设客户端发起如下请求:
text
PATCH /users/42?notify=true
Content-Type: application/json
Authorization: Bearer example-token
{"name":"Grace"}
在 Express 中可以这样读取:
javascript
app.patch('/users/:id', (req, res) => {
const userId = req.params.id;
const shouldNotify = req.query.notify;
const changes = req.body;
const authorization = req.headers.authorization;
res.json({ userId, shouldNotify, changes, authorization });
});
四类数据的区别如下:
| 属性 | 数据位置 | 示例结果 | 适合表达 |
|---|---|---|---|
req.params |
路径占位符 | { id: '42' } |
明确定位某个资源 |
req.query |
? 后的查询字符串 |
{ notify: 'true' } |
筛选、分页、排序、可选开关 |
req.body |
HTTP 请求体 | { name: 'Grace' } |
创建或修改时提交的主体数据 |
req.headers |
HTTP 请求头 | { authorization: 'Bearer ...' } |
认证、内容类型、客户端能力等元信息 |
其中 body、query、params 的核心区别,不只是读取方式不同,更在于它们承担的语义不同:路径参数标识"操作谁",查询参数描述"怎样查询或附加什么选项",请求体承载"要提交什么数据"。
3.2 三个容易踩中的请求数据误区
第一,路径参数和查询参数通常都是字符串。req.params.id 读取到的 42 默认是字符串,req.query.notify 读取到的 true 也未必是布尔值,因此参与计算或判断前要主动转换并校验。
第二,req.body 不是天然存在的可靠对象。应用需要先注册对应的解析中间件;客户端也要发送匹配的 Content-Type。即使解析成功,其中的数据仍然来自外部输入,不能直接用于数据库操作或权限判断。
第三,Node.js 会把常见请求头名称规范为小写,因此通常通过 req.headers.authorization、req.headers['content-type'] 读取。请求头适合携带元信息,不适合塞入大段业务数据。
所有来自
req的值都应视为不可信输入。 读取只是第一步,类型转换、格式校验、权限判断和长度限制同样属于接口边界的一部分。
4. Response 响应对象:状态码与数据如何返回前端
4.1 res 对象与常用响应方法
路由处理函数中的 res 是 Express 提供的 Response 响应对象,代表服务器准备返回给当前客户端的 HTTP 响应。Express 在 Node.js 原生响应对象之上增加了许多便捷方法,因此后端可以通过它设置状态码和响应头、写入响应体,并最终结束请求---响应周期。
res.json()、res.send() 等并不是普通的业务函数,而是响应对象上的方法。它们解决的问题是:把 JavaScript 中的处理结果转换成符合 HTTP 规则的响应并发送给客户端。 常用方法的职责如下:
| API | 是否直接结束响应 | 主要作用 | 典型场景 |
|---|---|---|---|
res.json(data) |
是 | 将数据序列化为 JSON,并设置 JSON 内容类型 | REST API 返回对象、数组或错误信息 |
res.send(data) |
是 | 根据数据类型发送字符串、HTML、Buffer 等内容 | 返回文本、HTML 或二进制内容 |
res.status(code) |
否 | 设置 HTTP 状态码,并返回 res 自身 |
与 json()、send()、end() 链式组合 |
res.redirect(url) |
是 | 返回重定向响应,默认使用 302 状态码 | 登录后跳转、旧地址迁移 |
res.sendStatus(code) |
是 | 设置状态码,并发送对应的简短状态文本 | 快速返回 404、403 等结果 |
res.end() |
是 | 直接结束响应,不再写入正文 | 返回 204 无内容响应 |
javascript
app.get('/health', (req, res) => {
res.send('OK');
});
app.get('/profile', (req, res) => {
res.json({ id: 1, name: 'Ada' });
});
app.post('/users', (req, res) => {
const user = { id: 2, ...req.body };
res.status(201).json({ data: user });
});
app.get('/old-home', (req, res) => {
res.redirect('/home');
});
app.delete('/sessions/current', (req, res) => {
res.status(204).end();
});
res.json() 会把对象或数组序列化为 JSON,并设置相应的 Content-Type,因此它是接口返回结构化数据时最清晰的选择。res.send() 更通用:传入字符串时可以返回文本或 HTML,传入 Buffer 时可以返回二进制内容。虽然 res.send() 也能处理部分 JavaScript 数据类型,但 API 返回对象或数组时优先使用 res.json(),能让代码意图更加明确。
res.status(201) 只负责把状态码设置为 201,并没有立即发送响应。因为该方法返回的仍是当前 res,所以可以继续调用 .json()。这就是 res.status().json() 能链式书写的原因,等价写法如下:
javascript
res.status(201);
res.json({ message: 'Created' });
res.redirect('/home') 会要求客户端访问新的地址,默认状态码为 302;需要表达永久迁移时,也可以使用 res.redirect(301, '/home')。res.sendStatus(404) 相当于快速设置 404 并发送简短的状态文本,而 res.status(404).json({ message: '资源不存在' }) 更适合需要统一 JSON 错误结构的接口。
4.2 状态码不是装饰,而是响应语义的一部分
前端通常会同时读取状态码和响应体。状态码用于快速表达请求结果,JSON 则携带更具体的数据或错误说明。
| 状态码 | 含义 | 常见使用场景 |
|---|---|---|
200 |
请求成功 | 查询、更新成功 |
201 |
资源创建成功 | POST 创建成功 |
204 |
成功但无响应体 | 删除成功且无需返回数据 |
400 |
请求不合法 | 参数格式错误、缺少必填项 |
401 |
未通过身份认证 | 未登录、令牌无效 |
403 |
已识别身份但无权限 | 普通用户访问管理功能 |
404 |
资源不存在 | 根据 ID 未查到目标 |
500 |
服务内部错误 | 未预期异常 |
一旦执行 res.json()、res.send()、res.redirect()、res.sendStatus() 或 res.end(),通常就应当 return 或结束当前处理逻辑。响应已经发送后再次发送,会触发"响应头已发送"一类错误。一条请求链最终只能完成一次有效响应。
javascript
app.get('/users/:id', (req, res) => {
const user = users.find((item) => item.id === Number(req.params.id));
if (!user) {
return res.status(404).json({ message: '用户不存在' });
}
return res.json({ data: user });
});
这里的 return 不是把响应数据返回给 Express,而是立即结束当前 JavaScript 函数,避免后面的 res.json() 再次执行。响应完成后也不应随意调用 next(),否则后续处理层可能继续修改一个已经结束的响应。
5. Express 中间件:一条按顺序推进的请求处理链
5.1 什么是 Middleware,app.use() 做了什么
中间件本质上是一个参与请求---响应周期的函数。普通中间件接收三个参数:req、res 和 next。
javascript
function requestLogger(req, res, next) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
app.use(requestLogger);
中间件不是夹在前端和后端之间的神秘服务,而是 Express 请求处理链上的一个函数节点。 它可以执行代码、读取或修改
req与res、直接结束响应,也可以调用next()继续向后传递控制权。
三个参数各自承担明确职责:
| 参数 | 含义 | 中间件中的常见用途 |
|---|---|---|
req |
当前请求对象 | 读取参数、挂载用户信息、记录请求上下文 |
res |
当前响应对象 | 设置响应头、直接拒绝请求、返回结果 |
next |
Express 提供的推进函数 | 继续普通流程或把异常交给错误流程 |
app.use() 用于把中间件注册到应用的处理栈中。不指定路径时,它会参与后续匹配到的各类请求;指定路径时,它只参与该挂载路径及其子路径下的请求。
javascript
app.use('/admin', (req, res, next) => {
console.log('进入管理模块');
next();
});
这个中间件会参与 /admin、/admin/users 等路径的请求,而且不限定 GET 或 POST。它的价值在于把日志、鉴权、跨域处理、请求体解析等横切逻辑从具体业务路由中抽离出来。
5.2 next() 为什么能进入下一层,执行顺序从哪里来
Express 在注册 app.use() 和 app.METHOD() 时,会按照代码执行顺序把对应处理层加入内部栈。请求到达后,调度器从前往后检查每一层:路径和请求方法满足条件时执行;不满足时继续寻找下一层。
next 不是开发者提前定义好的下一个业务函数,而是 Express 在调度当前层时传入的推进函数。它关联着当前请求的调度状态。调用 next() 后,Express 会从当前位置之后继续扫描处理栈,找到下一层符合条件的普通中间件或路由处理函数。
javascript
app.use((req, res, next) => {
console.log('1. 请求日志');
next();
});
app.use(express.json());
app.post('/users', (req, res) => {
console.log('2. 进入创建用户路由');
res.status(201).json({ data: req.body });
});
收到 POST /users 后,请求先经过日志中间件;日志中间件调用 next(),JSON 解析器随后处理请求体;解析器完成后继续推进,最终进入路由并发送响应。这里的顺序不是优先级猜测,而是注册顺序带来的确定结果。
如果把 app.use(express.json()) 放到路由后面,路由执行时解析器还没有运行,req.body 就无法按预期获得 JSON 对象。类似地,鉴权中间件放在受保护路由之后,也无法保护前面已经完成响应的路由。
一层处理函数通常有三种选择:
- 调用
next(),让普通处理链继续向后推进。 - 调用
res.json()、res.send()等方法结束响应,不再调用next()。 - 调用
next(error),离开普通流程并进入错误处理流程。
如果既不结束响应也不调用 next(),请求就会停在当前层,客户端持续等待。这正是很多接口"卡住不返回"的常见原因。
5.3 app.use(express.json()) 为什么仍然是在注册中间件
这行代码初看容易产生疑问:普通中间件明明写成 (req, res, next) => {},为什么 express.json() 没有显式传入三个参数?关键在于它包含两个不同阶段:
javascript
const jsonParser = express.json();
console.log(typeof jsonParser); // function
app.use(jsonParser);
第一阶段调用 express.json(),它根据配置创建并返回一个中间件函数 ;第二阶段由 app.use() 注册这个返回值。此时只是登记函数,并没有处理具体请求。等请求真正到达时,Express 才会调用 jsonParser(req, res, next)。
也可以给解析器传配置:
javascript
const jsonParser = express.json({ limit: '100kb' });
app.use(jsonParser);
这种"调用工厂函数,得到已配置中间件"的形式非常常见。express.json() 表面没有写 req、res、next,是因为这三个参数属于未来的请求处理阶段,而当前代码处于中间件创建与注册阶段。
5.4 自定义中间件与路由到底是什么关系
路由处理函数并不是中间件之外的另一套机制。app.get()、app.post() 注册的处理函数同样位于请求处理栈中,只是它们增加了更严格的匹配条件:既要匹配路径,也要匹配 HTTP 方法。
javascript
function requireAuth(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: '请先登录' });
}
req.user = { id: 1, role: 'admin' };
next();
}
app.get('/profile', requireAuth, (req, res) => {
res.json({ data: req.user });
});
在这个路由中,requireAuth 是路由级中间件。认证失败时,它直接返回 401,后面的业务处理函数不会执行;认证成功时,它把用户信息挂到 req.user,调用 next() 后,下一层便能复用这份上下文。
| 注册方式 | 匹配规则 | 典型职责 |
|---|---|---|
app.use(handler) |
不限定 HTTP 方法,参与后续请求 | 全局日志、通用解析 |
app.use('/api', handler) |
匹配路径前缀,不限定 HTTP 方法 | 模块级鉴权、模块级日志 |
app.get(path, handler) |
匹配 GET 与指定路径 | 查询业务 |
app.post(path, handler) |
匹配 POST 与指定路径 | 创建业务 |
app.METHOD(path, m1, m2) |
同一路由内按顺序执行多个处理函数 | 路由校验、权限检查、业务处理 |
因此,可以把 Express 应用理解为很多"条件不同、职责不同的中间件层"串起来的结果。全局中间件处理共性问题,路由级中间件处理局部前置条件,最后的业务处理函数生成响应。
6. 错误处理中间件:让异常沿另一条链统一收口
6.1 next() 与 next(error) 的本质区别
普通流程中,next() 表示当前层处理完毕,请继续寻找下一层普通中间件或路由处理函数;next(error) 则告诉 Express 当前请求发生了异常,需要切换到错误处理流程。
| 调用方式 | 调度含义 | 后续寻找目标 |
|---|---|---|
next() |
正常推进 | 下一个匹配的普通中间件或路由处理函数 |
next(error) |
进入错误流程 | 后续匹配的错误处理中间件 |
错误处理中间件必须保留四个参数,其函数签名是 (err, req, res, next):
javascript
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
message: err.message || '服务器内部错误'
});
});
Express 通过四参数形式识别错误处理中间件。即使当前逻辑没有直接使用最后一个 next,也应保留它,以免该函数被当成普通中间件。
next(error)不是立即向客户端发送错误,而是把错误和控制权交还给 Express 调度器。 最终由后面的错误处理中间件决定状态码、响应结构和日志策略。
6.2 从路由异常到统一错误响应的完整过程
下面的例子把"前后串联"的过程完整展现出来。路由只负责识别业务异常并向后传递,错误处理中间件负责统一生成响应。
javascript
async function findUser(id) {
if (id === '500') {
throw new Error('数据服务暂时不可用');
}
return id === '1' ? { id: 1, name: 'Ada' } : null;
}
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
const error = new Error('用户不存在');
error.statusCode = 404;
throw error;
}
res.json({ data: user });
} catch (error) {
next(error);
}
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
message: err.message || '服务器内部错误'
});
});
这段逻辑的执行顺序可以拆成七步:
- 路由处理函数开始执行,并在
try中等待查询结果。 - 查询失败,或者主动抛出"用户不存在"异常。
catch捕获异常并调用next(error)。- Express 切换到错误处理流程,跳过后续普通处理层。
- 调度器从当前位置继续向后寻找四参数错误处理中间件。
- 错误处理中间件根据异常设置 404 或 500 状态码。
res.status(statusCode).json(...)统一向客户端返回 JSON 错误信息。
res.headersSent 判断用于处理响应已经开始发送的特殊情况。此时再设置状态码或响应头可能引发二次错误,因此把异常继续交给 Express 的默认错误处理器更稳妥。
在 Express 5 中,如果返回 Promise 的路由处理函数发生拒绝或抛出异常,Express 能自动把异常交给错误流程;显式的 try/catch + next(error) 仍然有助于初学者看清传递过程,也便于兼容大量 Express 4 项目。无论采用哪种写法,核心都不变:让异常进入统一错误链,而不是在各处随意拼装响应。
6.3 为什么错误处理中间件通常放在路由后面
原因仍然是执行顺序。Express 的调度器从当前层向后寻找下一层,不会在发生异常后回到栈顶重新扫描。如果错误处理中间件注册在路由之前,而异常由后面的路由产生,它就无法接住这次异常。
推荐顺序如下:
javascript
app.use(express.json());
app.use(requestLogger);
app.get('/users/:id', getUser);
app.post('/users', createUser);
app.use(notFoundHandler);
app.use(errorHandler);
其中 notFoundHandler 可以在所有业务路由都未响应时创建 404 异常,最后的 errorHandler 则作为统一出口。
javascript
function notFoundHandler(req, res, next) {
const error = new Error('接口不存在');
error.statusCode = 404;
next(error);
}
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
message: err.message || '服务器内部错误'
});
}
统一错误处理比每个接口重复响应更合理,主要体现在以下方面:
- 响应结构一致:前端不必适配多种错误格式。
- 状态码策略集中:404、400、500 等映射更容易维护。
- 业务代码更聚焦:路由负责业务成功路径,异常交给统一出口。
- 日志与安全策略统一:生产环境可集中记录内部错误,同时避免把堆栈和敏感细节直接暴露给客户端。
统一处理并不意味着所有异常都变成 500。合理做法是给可预期的业务错误附加状态码或错误类型,再由错误处理中间件完成最终映射;无法识别的异常才回退到 500。
7. 串起 Express 的完整请求生命周期
7.1 一次请求如何穿过普通链与错误链
到这里,可以把各类 API 放回同一幅运行图景中:
text
客户端发送 HTTP 请求
↓
Express 接收请求并创建增强后的 req、res
↓
按注册顺序进入 app.use() 中间件
↓
express.json() 解析 JSON,请求日志与鉴权继续处理
↓
app.get() / app.post() 等路由匹配方法与路径
↓
正常:res.status().json() 发送结果并结束周期
↓
异常:catch 捕获后调用 next(error)
↓
Express 跳过普通层,寻找后续错误处理中间件
↓
统一设置状态码并通过 res.json() 返回错误信息
↓
客户端收到 HTTP 响应
需要注意,正常分支和异常分支不会在同一次处理中全部执行。正常情况下,路由发送响应后周期结束;异常情况下,next(error) 把请求切换到错误链,由错误处理中间件完成响应。
7.2 用一套简化示例完成最终复盘
下面是一套可以直接理解各层位置的简化结构。重点不是业务复杂度,而是观察注册顺序和每一层的职责:
javascript
const express = require('express');
const app = express();
const PORT = 3000;
const HOST = '127.0.0.1';
// 1. 内置解析中间件:先把 JSON 转成 req.body
app.use(express.json());
// 2. 自定义普通中间件:记录请求后继续向下
app.use((req, res, next) => {
console.log(`${req.method} ${req.originalUrl}`);
next();
});
// 3. 路由:匹配方法和路径,处理正常业务
app.post('/users', (req, res, next) => {
try {
const { name } = req.body;
if (!name) {
const error = new Error('name 不能为空');
error.statusCode = 400;
throw error;
}
const user = { id: Date.now(), name };
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
});
// 4. 未匹配任何业务路由时,创建 404 异常
app.use((req, res, next) => {
const error = new Error('接口不存在');
error.statusCode = 404;
next(error);
});
// 5. 错误处理中间件:必须放在业务路由之后
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
message: err.message || '服务器内部错误'
});
});
// 6. 启动服务器
app.listen(PORT, HOST, () => {
console.log(`Server is running at http://${HOST}:${PORT}`);
});
这段代码中,express.json()、日志函数、POST 路由、404 处理和统一错误处理都属于处理链的一部分,只是匹配条件与职责不同。请求体合法时,POST 路由通过 201 和 JSON 结束响应;缺少 name 时,异常经 next(error) 跳转到错误链,以 400 返回;路径不匹配时,404 层主动创建异常,同样交给统一出口。
| 阶段 | 关键 API | 是否继续向后 | 主要产物 |
|---|---|---|---|
| 接收并解析 | app.use(express.json()) |
解析成功后继续 | req.body |
| 通用处理 | app.use((req, res, next) => {}) |
通常调用 next() |
日志、身份、上下文 |
| 路由匹配 | app.get()、app.post() 等 |
成功时通常结束响应 | 业务结果 |
| 异常传递 | next(error) |
切换到错误链 | 错误对象 |
| 统一响应 | app.use((err, req, res, next) => {}) |
通常结束响应 | 状态码与 JSON 错误信息 |
总结
Express 的常用 API 并不是彼此孤立的工具。express() 创建应用,app.listen() 让应用开始接收网络请求;express.json() 等中间件先加工请求,app.get()、app.post() 等路由再依据 HTTP 方法和路径选择业务逻辑;处理函数从 req.params、req.query、req.body 和 req.headers 读取不同语义的数据,最后通过 res.status()、res.json() 或 res.send() 完成响应。
更重要的是,Express 应被理解为一条有顺序的请求处理链。普通中间件用 next() 继续正常流程,路由可以直接结束响应,异常则通过 next(error) 进入四参数错误处理中间件。注册顺序决定请求先经过谁、后经过谁,也决定解析、鉴权和错误处理能否在正确位置生效。掌握这条主线后,再面对日志、认证、参数校验、路由拆分等工程需求,就能清楚判断每段逻辑应该放在哪一层。