Express — 中间件管道、路由与 API 端点测试¶
这一章讲什么: 上一章用纯 Node 手写服务器,被 if-else 路由和手动拼流折磨够了; 这一章给出工业界标准答案 Express——以及它的核心机制中间件。 读完你会理解「管道」这个贯穿全书的模型:后面章节的认证、安全、日志、错误处理, 全都是不同种类的中间件往这条管子里插。
1. Express 是什么:Node 之上的一层,请求的一条管道¶
Express 自己不是服务器——app.listen 内部调的仍是 Node 的 createServer
和 listen1。它提供的是两样东西:一条中间件管道,和一个丰富到
现成的中间件生态2。
中间件(middleware) 就是插在请求处理流程里的拦截函数:
能查看、修改 Request 和 Response,完事后调用 next() 把控制权
递给下一个;不调 next,请求就挂死在这条中间件里,永远到不了后面的
路由3。整个处理过程是严格按声明顺序串行的4:
请求进来
│
▼
① 应用级中间件(app.use,对所有路由生效) ← express.json()、日志、认证…
│ next()
▼
② 路由级中间件(router.get/post/…,只对匹配的路由生效)
│
▼
③ 路由处理器:res.send / res.json 给出最终答复
│
▼ (或:任何一步 throw → 跳到 ④)
④ 错误处理中间件(四个参数的那个):兜住异常,回 500
图说:一条流水线。next() 是传送带,send 是终点,throw 是跳闸。
res.send 比 Node 裸的 res.end 多走一步:自动带上 Content-Type:
text/html 头——Node 里这要自己显式设5。
主线例子:一场「路由怎么也进不去」的怪事¶
照书里的步骤搭一个服务器,加入两段应用级中间件(第一段只打印日志并 next(),
第二段 res.send("Hello world. I am custom middleware.")),
再把 router.get("/a", …) 挂上去。打开 /a,预期看到 "Hello this is route a",
实际看到的却是第二段中间件的文字6。
怪事的原因就是上面图里的两条规则撞在一起:
- 应用级中间件总是先跑(它在管道更上游);
- 第二段中间件调了
res.send——发送即终止,响应在那里就结束了, 根本轮不到后面的 router6。
把第二段注释掉,/a 立刻正常;再访问 /c 得到 Cannot GET /c,
因为浏览器地址栏发的永远是 GET,而 /c 注册的是 POST-only——
curl 补一个 -X POST 就通7。这三个连环小事故,比任何定义都更能让人
记住「顺序、终止、动词匹配」三件事。
2. JSON 解析与错误中间件:Express 的两个标配零件¶
app.use(express.json()) 一行,让请求体里的 JSON 自动解析好、
挂到 req.body 上(老版本这要装单独的 body-parser 包)8。
主线例子的 /c 路由由此能读到 req.body.message8。
错误处理中间件的识别规则全书数一数二的「坑点」:它有四个参数
(err, req, res, next),第一个是这个错误的 Error 对象。
这不是可选的签名装饰——Express 就是靠「数参数」认出错误中间件的;
少写了 err,它就当一个普通中间件注册,永远接不到错误9。
管道里任何一步 throw,控制流(程序执行的走向)直接跳到管道末尾的错误中间件10。
3. Web API 与 REST:把 HTTP 动词映射成数据操作¶
Web API 就是用 Web 技术向其他系统提供编程服务的接口;
最主流的架构风格是 REST:HTTP 动词对应数据操作(POST 创建、
GET 读取、PUT/PATCH 更新、DELETE 删除),数据通常用 JSON 交换11。
端点(endpoint)是一个 URI(统一资源标识符,这里即路径),必须稳定;要改就发新版本——
/api/v1/users 的下一版是 /api/v2/users,旧地址原样保留12。
Express 里路径里的 :<name> 是 param(路径参数):路由
/api/v1/users/:id 配 req.params.id 取值——GET 的参数从 URL 来;
POST 的参数从 req.body 来13。书里的诚实注脚:groups 例子用 POST
取数只为演示读请求体,真实 API 取数该用 GET——REST 约定要遵守14。
4. 测 Node:内置运行器 + supertest¶
服务端测试的底座是 Node 内置测试运行器(node:test,Node 20 稳定):
随 Node 附带、零依赖零配置,node --test 自动跑所有 test/spec 文件15。
和 Vitest 像,但有三处不同要习惯:
- 断言用
assert不是expect:assert.equal(宽松相等,带类型强制的==)、assert.strictEqual(严格相等,对象比的是同一内存地址)、assert.deepEqual/deepStrictEqual(逐属性比,宽松/严格两版)、assert.throws(断言抛错)16。 - 跳过/单跑是参数不是方法链:
describe.skip跳过;某个测试传{ only: true },但必须加--test-only标志才生效—— 而且only会跳过整个文件里所有其它测试17。 - mock 有两个作用域:全局
mock.fn(...)配mock.reset()(用完必须复位, 否则污染同文件其它测试);测试上下文ctx.mock.method(对象, "方法名", 替代实现)只在本测试内生效,还能断言调用次数(mock.callCount()),不用复位18。
端点测试本该「把服务器跑起来再发请求」——supertest 把这一步也省了:
request(app) 直接吃 Express 的 app 实例,内部模拟一次完整的服务器往返19:
const res = await request(app)
.get("/api/v1/users")
.expect("Content-Type", /json/)
.expect(200);
assert.equal(res.body.length, 3);
.get/.post 选动词和 URL,.expect 链式断言状态码与头部,
POST 用 .send({...}) 填 JSON 体;测完 after(() => server.close())
把服务器关掉20。一个值得记的细节:书里的 maxId() 是从 routes 模块
导出后再被测试引用的——测试和实现共享同一份假数据数组,断言才敢写
assert.equal(res.body.id, 4) 这种具体数字21。
5. 作者的判断与证据¶
- 「Express 让 Node 开发更简单清晰」由对照(第 8 章的手写受罪)支撑,非空口2。
- 「错误中间件必须四个参数」是框架的硬规则,作者用「少写就永远接不到错误」 的警告式表述强调9。
- 「REST 约定应遵守;取数据用 GET」在 POST 演示之后立刻自我纠偏,口径干净14。
6. 边界与局限¶
- TypeScript 缺席:本章全用 JavaScript 写 Express(「先专注学 Express」),
类型化的 Express 写法(如
Request<{}, {}, Body>泛型)要到后面章节才出现。 - 异步错误处理没展开:Express 5 里 rejected promise 会自动转给错误中间件
(这是 5.x 相对 4.x 的重要行为变化),本章只在代码里用了 try/catch +
next(e), 没讲机制变化。 - supertest 只演示了 JSON 端点:文件上传、cookie、认证头等真实场景没覆盖 (第 13、14 章的认证测试会部分补上)。
判断(我们的,不是书里的): 「路由进不去」的三连环事故(被 send 截胡、 GET/POST 不匹配、curl 救场)是本章最有记忆点的设计——它一次性把「顺序、 终止、动词」三条规则钉进一个具体场景。教学上这比先给中间件定义再举正面 例子有效,因为困惑(我明明注册了路由)先于解释出现。 如果错,会错在: 依赖「事故驱动」的学习路径对「喜欢先建立完整模型再动手」 的读者不友好;这类读者可能更想要一张完整的管道生命周期图,而本书选择 让他们先踩三个坑再总结。
7. 可带走的¶
- Express = Node 上的中间件管道;
app.listen底下还是 Node 的 server。 - 中间件必须调
next()否则请求挂死;res.send终止整条管道。 - 应用级中间件先于路由级;管道顺序就是代码声明顺序。
express.json()把 JSON 体解析到req.body。- 错误中间件 = 四个参数
(err, req, res, next);Express 靠数参数认它。 - REST:动词映射数据操作;端点 URI 稳定,改版走
/v2/。 /:id是路径参数(req.params),POST 参数走req.body。node:test零依赖:--test跑全部,--test-only配合{only:true}; 断言用 assert 家族(equal/strictEqual/deepStrictEqual/throws)。- 全局 mock 要 reset;
ctx.mock.method测试级、免复位、能数调用。 - supertest 不发真网络:
request(app).get(...).expect(200)模拟完整往返。
8. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| Express 是 Node 上的一层 | Chapter 9: Learning Express | text/44-ch09-chapter-9-learning-express.txt:71(搜「sits on top of Node」) |
| 中间件与 next() | 同上 | text/44-ch09-chapter-9-learning-express.txt:75(搜「plug into the request processing」) · text/44-ch09-chapter-9-learning-express.txt:139(搜「fails to call next」) |
| 顺序语义 | 同上 | text/44-ch09-chapter-9-learning-express.txt:133(搜「sequential order」) |
| 路由被 send 截胡的事故 | 同上 | text/44-ch09-chapter-9-learning-express.txt:474(搜「hierarchy order matters」) · text/44-ch09-chapter-9-learning-express.txt:482(搜「all calls end there」) |
| Cannot GET /c 与 curl | 同上 | text/44-ch09-chapter-9-learning-express.txt:520(搜「Cannot GET」) |
| express.json 与 req.body | 同上 | text/44-ch09-chapter-9-learning-express.txt:583(搜「express.json」) |
| 错误中间件四参数 | 同上 | text/44-ch09-chapter-9-learning-express.txt:669(搜「four parameters」) · text/44-ch09-chapter-9-learning-express.txt:675(搜「never catch errors」) |
| REST 与版本化 URI | 同上 | text/44-ch09-chapter-9-learning-express.txt:729(搜「Representational State Transfer」) · text/44-ch09-chapter-9-learning-express.txt:741(搜「version update」) |
| param 与 req.params | 同上 | text/44-ch09-chapter-9-learning-express.txt:838(搜「param」) |
| 取数该用 GET 的自我纠偏 | 同上 | text/44-ch09-chapter-9-learning-express.txt:988(搜「demonstrating」) · text/44-ch09-chapter-9-learning-express.txt:992(搜「should use the GET method」) |
| Node 测试运行器 | 同上 | text/44-ch09-chapter-9-learning-express.txt:1008(搜「built-in Node test runner」) |
| only 需要 --test-only | 同上 | text/44-ch09-chapter-9-learning-express.txt:1204(搜「another flag」) |
| assert 断言家族 | 同上 | text/44-ch09-chapter-9-learning-express.txt:1261(搜「This first test is doing a loose equalit」) · text/44-ch09-chapter-9-learning-express.txt:1321(搜「deepStrictEqual」) |
| mock.reset 与 ctx.mock | 同上 | text/44-ch09-chapter-9-learning-express.txt:1465(搜「mock.reset」) · text/44-ch09-chapter-9-learning-express.txt:1485(搜「ctx.mock.method(person, "speak", () => s」) |
| supertest 模拟服务器 | 同上 | text/44-ch09-chapter-9-learning-express.txt:1589(搜「skip this step」) · text/44-ch09-chapter-9-learning-express.txt:1884(搜「simulates a running server」) |
-
出处:「Chapter 9: Learning Express」第 237–259 段(
text/44-ch09-chapter-9-learning-express.txt:251,搜「calls Node's」)。 ↩ -
出处:同上第 67–77 段(
text/44-ch09-chapter-9-learning-express.txt:71,搜「layer of code」;:73,搜「rich ecosystem」)。 ↩↩ -
出处:同上第 123–139 段(
text/44-ch09-chapter-9-learning-express.txt:125,搜「interceptor」;:137,搜「next function」;:139,搜「hang」)。 ↩ -
出处:同上第 133 段(
text/44-ch09-chapter-9-learning-express.txt:133,搜「sequential order」)。 ↩ -
出处:同上第 328–348 段(
text/44-ch09-chapter-9-learning-express.txt:342,搜「content-type header」)。 ↩ -
出处:同上第 448–508 段(
text/44-ch09-chapter-9-learning-express.txt:474,搜「hierarchy order matters」;:482,搜「all calls end there」)。 ↩↩ -
出处:同上第 510–567 段(
text/44-ch09-chapter-9-learning-express.txt:520,搜「Cannot GET」;:536,搜「POST-only」)。 ↩ -
出处:同上第 569–618 段(
text/44-ch09-chapter-9-learning-express.txt:583,搜「express.json」;:597,搜「req.body.message」)。 ↩↩ -
出处:同上第 647–675 段(
text/44-ch09-chapter-9-learning-express.txt:669,搜「four parameters」;:675,搜「never catch errors」)。 ↩↩ -
出处:同上第 677–701 段(
text/44-ch09-chapter-9-learning-express.txt:697,搜「This is why after our first middleware t」)。 ↩ -
出处:同上第 715–735 段(
text/44-ch09-chapter-9-learning-express.txt:729,搜「Representational State Transfer」;:735,搜「JSON format」)。 ↩ -
出处:同上第 737–751 段(
text/44-ch09-chapter-9-learning-express.txt:741,搜「version update」)。 ↩ -
出处:同上第 782–868 段(
text/44-ch09-chapter-9-learning-express.txt:838,搜「param」;:866,搜「req.params.id」)。 ↩ -
出处:同上第 984–992 段(
text/44-ch09-chapter-9-learning-express.txt:992,搜「should use the GET method」)。 ↩↩ -
出处:同上第 1002–1012 段与 1168–1176 段(
text/44-ch09-chapter-9-learning-express.txt:1008,搜「built-in Node test runner」;:1172,搜「node --test」)。 ↩ -
出处:同上第 1228–1359 段(
text/44-ch09-chapter-9-learning-express.txt:1261,搜「This first test is doing a loose equalit」;:1296,搜「same memory address」;:1321,搜「deepStrictEqual」;:1356,搜「throws」)。 ↩ -
出处:同上第 1088–1226 段(
text/44-ch09-chapter-9-learning-express.txt:1144,搜「skipped」;:1204,搜「another flag」;:1222,搜「entire file」)。 ↩ -
出处:同上第 1383–1553 段(
text/44-ch09-chapter-9-learning-express.txt:1465,搜「mock.reset」;:1517,搜「ctx.mock」;:1488,搜「callCount」)。 ↩ -
出处:同上第 1579–1591 段(
text/44-ch09-chapter-9-learning-express.txt:1589,搜「skip this step」)。 ↩ -
出处:同上第 1832–1996 段(
text/44-ch09-chapter-9-learning-express.txt:1846,搜「server.close」;:1884,搜「simulates a running server」;:1974,搜「send」)。 ↩ -
出处:同上第 1838–1844 段与 1998 段后(
text/44-ch09-chapter-9-learning-express.txt:1844,搜「maxId」)。 ↩