编程语言研究书库

Express — 中间件管道、路由与 API 端点测试

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。

怪事的原因就是上面图里的两条规则撞在一起:

  1. 应用级中间件总是先跑(它在管道更上游);
  2. 第二段中间件调了 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. 可带走的

  1. Express = Node 上的中间件管道;app.listen 底下还是 Node 的 server。
  2. 中间件必须调 next() 否则请求挂死;res.send 终止整条管道。
  3. 应用级中间件先于路由级;管道顺序就是代码声明顺序。
  4. express.json() 把 JSON 体解析到 req.body。
  5. 错误中间件 = 四个参数 (err, req, res, next);Express 靠数参数认它。
  6. REST:动词映射数据操作;端点 URI 稳定,改版走 /v2/。
  7. /:id 是路径参数(req.params),POST 参数走 req.body。
  8. node:test 零依赖:--test 跑全部,--test-only 配合 {only:true}; 断言用 assert 家族(equal/strictEqual/deepStrictEqual/throws)。
  9. 全局 mock 要 reset;ctx.mock.method 测试级、免复位、能数调用。
  10. supertest 不发真网络:request(app).get(...).expect(200) 模拟完整往返。

8. 原文地图

主题原书章原文位置
Express 是 Node 上的一层Chapter 9: Learning Expresstext/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」)

Footnotes

  1. 出处:「Chapter 9: Learning Express」第 237–259 段(text/44-ch09-chapter-9-learning-express.txt:251,搜「calls Node's」)。 ↩

  2. 出处:同上第 67–77 段(text/44-ch09-chapter-9-learning-express.txt:71,搜「layer of code」;:73,搜「rich ecosystem」)。 ↩ ↩2

  3. 出处:同上第 123–139 段(text/44-ch09-chapter-9-learning-express.txt:125,搜「interceptor」;:137,搜「next function」;:139,搜「hang」)。 ↩

  4. 出处:同上第 133 段(text/44-ch09-chapter-9-learning-express.txt:133,搜「sequential order」)。 ↩

  5. 出处:同上第 328–348 段(text/44-ch09-chapter-9-learning-express.txt:342,搜「content-type header」)。 ↩

  6. 出处:同上第 448–508 段(text/44-ch09-chapter-9-learning-express.txt:474,搜「hierarchy order matters」;:482,搜「all calls end there」)。 ↩ ↩2

  7. 出处:同上第 510–567 段(text/44-ch09-chapter-9-learning-express.txt:520,搜「Cannot GET」;:536,搜「POST-only」)。 ↩

  8. 出处:同上第 569–618 段(text/44-ch09-chapter-9-learning-express.txt:583,搜「express.json」;:597,搜「req.body.message」)。 ↩ ↩2

  9. 出处:同上第 647–675 段(text/44-ch09-chapter-9-learning-express.txt:669,搜「four parameters」;:675,搜「never catch errors」)。 ↩ ↩2

  10. 出处:同上第 677–701 段(text/44-ch09-chapter-9-learning-express.txt:697,搜「This is why after our first middleware t」)。 ↩

  11. 出处:同上第 715–735 段(text/44-ch09-chapter-9-learning-express.txt:729,搜「Representational State Transfer」;:735,搜「JSON format」)。 ↩

  12. 出处:同上第 737–751 段(text/44-ch09-chapter-9-learning-express.txt:741,搜「version update」)。 ↩

  13. 出处:同上第 782–868 段(text/44-ch09-chapter-9-learning-express.txt:838,搜「param」;:866,搜「req.params.id」)。 ↩

  14. 出处:同上第 984–992 段(text/44-ch09-chapter-9-learning-express.txt:992,搜「should use the GET method」)。 ↩ ↩2

  15. 出处:同上第 1002–1012 段与 1168–1176 段(text/44-ch09-chapter-9-learning-express.txt:1008,搜「built-in Node test runner」;:1172,搜「node --test」)。 ↩

  16. 出处:同上第 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」)。 ↩

  17. 出处:同上第 1088–1226 段(text/44-ch09-chapter-9-learning-express.txt:1144,搜「skipped」;:1204,搜「another flag」;:1222,搜「entire file」)。 ↩

  18. 出处:同上第 1383–1553 段(text/44-ch09-chapter-9-learning-express.txt:1465,搜「mock.reset」;:1517,搜「ctx.mock」;:1488,搜「callCount」)。 ↩

  19. 出处:同上第 1579–1591 段(text/44-ch09-chapter-9-learning-express.txt:1589,搜「skip this step」)。 ↩

  20. 出处:同上第 1832–1996 段(text/44-ch09-chapter-9-learning-express.txt:1846,搜「server.close」;:1884,搜「simulates a running server」;:1974,搜「send」)。 ↩

  21. 出处:同上第 1838–1844 段与 1998 段后(text/44-ch09-chapter-9-learning-express.txt:1844,搜「maxId」)。 ↩

On this page