Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Node.js 与框架 / Node.js & Frameworks

1. Node.js 与后端框架学习路线 / Node.js and Backend Frameworks Learning Path

2. Node.js 运行时内部原理 / Node.js Runtime Internals

3. Node.js 异步模式与错误处理 / Async Patterns and Error Handling in Node.js

4. Express.js 深度剖析 / Express.js Deep Dive

5. Express 高级模式与生产实践 / Express Advanced Patterns and Production Practices

6. 现代 Node.js 框架对比 / Modern Node.js Framework Comparison

7. RESTful API 设计原则与实践 / RESTful API Design Principles and Practice

8. Node.js 环境配置与 12-Factor App / Environment Configuration and 12-Factor App

本页目录

Express.js 深度剖析 / Express.js Deep Dive ​

📅 创建时间:2026-07-28 🏷️ 标签:#Express #Middleware #OnionModel #Router #ErrorHandling #TypeScript #Backend 📚 前置知识:[[00-overview]]


📋 本章目标 ​

  • 理解 Express "最小化抽象,最大化灵活性"的核心哲学,明确它与 Koa/Fastify 的定位差异
  • 深度掌握洋葱模型——中间件执行顺序、next() 的本质、next(err) 的错误跳转机制
  • 追踪完整的请求/响应生命周期:从 app.listen 到 res.json,理解 req/res 的增强过程
  • 掌握路由系统的三个层级:app.use、app.METHOD、Router,以及子路由挂载与模块化
  • 熟练配置生产级中间件栈:cors、helmet、compression、morgan、rate-limit、body parser
  • 掌握自定义中间件的设计模式:认证、日志、验证、错误处理、工厂函数
  • 理解 Express 4 与 Express 5 的错误处理差异,设计健壮的全局错误处理器
  • 能够使用 TypeScript 编写类型安全的 Express 应用,包括声明合并与 Zod 验证的类型推导

第1部分:Express 的核心哲学 ​

1.1 "最小化抽象,最大化灵活性" ​

Express 的设计哲学可以用一句话概括:它只做两件事——路由匹配和中间件调度,其余全部交给社区。

┌─────────────────────────────────────────────────────────────┐
│                Express 的核心能力边界                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Express 负责:                                       │   │
│  │  • 路由匹配 —— URL + HTTP Method → Handler           │   │
│  │  • 中间件调度 —— 洋葱模型串联 req → res 流程         │   │
│  │  • req/res 增强 —— 在原生对象上添加便捷方法          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Express 不负责:                                     │   │
│  │  • 数据验证(交给 Zod / Joi)                        │   │
│  │  • 身份认证(交给 Passport / 自定义中间件)          │   │
│  │  • 数据库 ORM(交给 Prisma / Drizzle)              │   │
│  │  • 文件上传(交给 multer / busboy)                  │   │
│  │  • 模板渲染(交给 EJS / Pug,Express 只提供胶水)    │   │
│  │  • 日志、限流、队列、缓存……(全部交给中间件)        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

1.2 Express vs Koa vs Fastify 定位对比 ​

┌─────────────────────────────────────────────────────────────┐
│               Node.js HTTP 框架光谱                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  轻量/灵活 ◄────────────────────────────► 功能/约束          │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │  http    │  │  Koa     │  │ Express  │  │ NestJS   │   │
│  │  module  │  │          │  │          │  │          │   │
│  ├──────────┤  ├──────────┤  ├──────────┤  ├──────────┤   │
│  │ 零抽象   │  │ async     │  │ 中间件   │  │ 装饰器   │   │
│  │ 手工一切 │  │ 洋葱模型  │  │ 生态最大 │  │ 依赖注入 │   │
│  │ 无生态   │  │ 生态小    │  │ 3700万   │  │ 全栈框架 │   │
│  │          │  │ 需要组装  │  │ 周下载   │  │ 企业级   │   │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘   │
│                                                             │
│  ┌──────────────────────────────────────────────────┐       │
│  │  Fastify                                           │       │
│  ├──────────────────────────────────────────────────┤       │
│  │  • 性能优先(比 Express 快 2-3 倍)                │       │
│  │  • 内置 Schema 验证(JSON Schema)                 │       │
│  │  • 插件系统(封装性好,类似 Koa 的 ctx)           │       │
│  │  • 适合:高性能 API、微服务、Serverless            │       │
│  └──────────────────────────────────────────────────┘       │
│                                                             │
│  选型建议:                                                   │
│  • Express → 生态优先、团队熟悉、快速原型、存量项目           │
│  • Fastify → 性能优先、新项目、需要内置验证和类型安全         │
│  • Koa    → 需要极致灵活、自行组装中间件层                   │
│  • NestJS → 大型企业应用、需要 OOP 架构和依赖注入            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

1.3 最小示例:Express 的 Hello World ​

typescript
import express, { Request, Response } from "express";

const app = express();

app.get("/", (req: Request, res: Response) => {
  res.json({ message: "Hello World" });
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});
1
2
3
4
5
6
7
8
9
10
11

这 7 行代码背后隐藏着 Express 的全部核心机制——路由注册、中间件执行、请求处理、响应发送。接下来我们将逐层拆解。


第2部分:洋葱模型深度图解 ​

2.1 什么是洋葱模型? ​

洋葱模型是 Express 中间件执行顺序的形象描述:请求从外层中间件进入,逐层穿透到达核心处理逻辑,响应再逐层穿出。

┌─────────────────────────────────────────────────────────────┐
│                    洋葱模型执行顺序                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│         请求进来                    响应出去                 │
│            │                          ▲                      │
│            ▼                          │                      │
│   ┌──────────────────────────────────────────┐              │
│   │  middleware1 前置逻辑                      │              │
│   │  ┌────────────────────────────────────┐   │              │
│   │  │  middleware2 前置逻辑                │   │              │
│   │  │  ┌──────────────────────────────┐  │   │              │
│   │  │  │  middleware3 前置逻辑          │  │   │              │
│   │  │  │  ┌────────────────────────┐  │  │   │              │
│   │  │  │  │  核心 Handler           │  │  │   │              │
│   │  │  │  │  (route handler)       │  │  │   │              │
│   │  │  │  └────────────────────────┘  │  │   │              │
│   │  │  │  middleware3 后置逻辑          │  │   │              │
│   │  │  └──────────────────────────────┘  │   │              │
│   │  │  middleware2 后置逻辑                │   │              │
│   │  └────────────────────────────────────┘   │              │
│   │  middleware1 后置逻辑                      │              │
│   └──────────────────────────────────────────┘              │
│                                                             │
│  代码表示:                                                  │
│  app.use((req, res, next) => {                              │
│    console.log("1: 进入 middleware1")     // ← 前置          │
│    next()                                // ← 进入下一层    │
│    console.log("1: 离开 middleware1")     // ← 后置          │
│  })                                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

2.2 next() 的本质 ​

next() 不是"执行下一个中间件",而是**"把控制权交还给 Express 调度器,调度器决定下一个要执行的中间件"**。

typescript
// next() 的本质——伪代码还原
function createNext(
  currentMiddlewareIndex: number,
  middlewareStack: Middleware[],
  req: Request,
  res: Response
): NextFunction {
  return (err?: any) => {
    if (err) {
      // 跳过普通中间件,直接找到错误处理中间件(4 参数签名)
      return processError(err, middlewareStack, req, res);
    }

    const nextIndex = currentMiddlewareIndex + 1;
    if (nextIndex >= middlewareStack.length) {
      // 栈结束,Express 内部发送 404 或完成响应
      return;
    }

    const nextMiddleware = middlewareStack[nextIndex];
    try {
      // 递归调用下一个中间件
      nextMiddleware(req, res, createNext(nextIndex, middlewareStack, req, res));
    } catch (syncError) {
      // Express 5 同时捕获同步和异步错误
      processError(syncError, middlewareStack, req, res);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

2.3 next(err) —— 跳过洋葱层 ​

┌─────────────────────────────────────────────────────────────┐
│                 next(err) 错误短路机制                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  正常流程:                                                  │
│  mw1 → mw2 → mw3 → handler → mw3后 → mw2后 → mw1后         │
│                                                             │
│  错误短路(mw2 中 next(err)):                              │
│  mw1 → mw2 → next(err)                                      │
│              │                                               │
│              ├── 跳过 mw3                                   │
│              ├── 跳过 handler                               │
│              ├── 跳过 mw3后、mw2后、mw1后                   │
│              │                                               │
│              └── 直接进入  errorHandler(4参数中间件)       │
│                                                             │
│  关键规则:                                                   │
│  • next(err) 跳过所有后续普通中间件                           │
│  • 只执行具有 (err, req, res, next) 签名的错误处理中间件     │
│  • 如果没有任何错误处理器 → Express 内置默认错误处理         │
│  • 错误处理器中调用 next() 无参数 → 回到正常流程             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript
// 洋葱模型完整演示
import express from "express";
const app = express();

// 中间件 1:最外层
app.use((req, res, next) => {
  console.log("1: 进入 mw1");
  res.locals.trace = ["mw1-enter"];
  next();
  console.log("1: 离开 mw1");
});

// 中间件 2:可能产生错误的中间件
app.use((req, res, next) => {
  console.log("2: 进入 mw2");
  res.locals.trace.push("mw2-enter");

  // 模拟:未认证用户触发短路
  if (!req.headers.authorization) {
    return next(new Error("未认证")); // 跳过后续所有正常中间件
  }

  next();
  console.log("2: 离开 mw2"); // 错误时这行不会执行
});

// 中间件 3:正常情况下会执行的中间件
app.use((req, res, next) => {
  console.log("3: 进入 mw3");
  res.locals.trace.push("mw3-enter");
  next();
  console.log("3: 离开 mw3");
});

// 路由处理器
app.get("/", (req, res) => {
  res.json({ trace: res.locals.trace });
});

// 错误处理中间件(4 参数签名)
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error("错误:", err.message);
  res.status(401).json({
    error: err.message,
    trace: res.locals.trace, // 只包含 mw1-enter, mw2-enter
  });
});

app.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

第3部分:请求/响应生命周期 ​

3.1 req / res 对象的增强过程 ​

┌─────────────────────────────────────────────────────────────┐
│              req / res 对象的层层增强                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第0层:node:http 原生对象                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  req: http.IncomingMessage                           │   │
│  │  • req.url, req.method, req.headers (只读流)       │   │
│  │  res: http.ServerResponse                           │   │
│  │  • res.writeHead(), res.end() (底层写流)           │   │
│  └─────────────────────────────────────────────────────┘   │
│                         ↓                                    │
│  第1层:Express 内置增强                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  req 新增:                                           │   │
│  │  • req.params  —— 路由参数 {id: "123"}              │   │
│  │  • req.query   —— 查询字符串 {page: "1"}            │   │
│  │  • req.path    —— 路径 "/users/123"                 │   │
│  │  • req.baseUrl —— 挂载前缀 "/api"                   │   │
│  │  • req.route   —— 当前匹配的路由对象                 │   │
│  │  • req.ip      —— 客户端 IP(支持 trust proxy)     │   │
│  │  • req.get()   —— 获取请求头(大小写不敏感)         │   │
│  │  • req.xhr     —— 是否 AJAX 请求                     │   │
│  │  res 新增:                                           │   │
│  │  • res.json()    —— JSON 响应 + 自动设置头          │   │
│  │  • res.send()    —— 智能响应(自动推断 Content-Type)│   │
│  │  • res.status()  —— 设置状态码(链式调用)           │   │
│  │  • res.redirect()—— 重定向                          │   │
│  │  • res.download()—— 文件下载                         │   │
│  │  • res.format()  —— 内容协商                         │   │
│  │  • res.locals    —— 请求级变量容器                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                         ↓                                    │
│  第2层:中间件注入的属性                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  express.json()    → req.body (已解析的 JSON)     │   │
│  │  cookie-parser     → req.cookies, req.signedCookies│   │
│  │  multer            → req.file, req.files            │   │
│  │  express-session   → req.session                    │   │
│  │  passport          → req.user, req.isAuthenticated()│   │
│  │  自定义中间件       → req.requestId, req.tenant     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

3.2 完整生命周期:从 app.listen 到 res.json ​

typescript
// 1. app.listen(3000) 内部做了什么
// → 调用 http.createServer(app) —— app 本身就是一个 requestListener
// → server.listen(3000) —— 监听 TCP 端口
// → 当请求到达时,app(req, res) 被调用

// 2. app(req, res) 内部流程
// → 创建内部 next 函数(闭包捕获 middleware stack 和当前 index)
// → 从第 0 个中间件开始执行
// → 洋葱模型逐层穿透

// 3. 路由匹配
// → 按注册顺序遍历路由
// → URL 匹配 + HTTP Method 匹配
// → 找到 handler,执行

// 4. res.json({ data: "hello" })
// → 内部调用 JSON.stringify()
// → 设置 Content-Type: application/json; charset=utf-8
// → 设置 Content-Length
// → 调用底层 res.end(jsonString)
// → 响应发送到客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第4部分:路由系统 ​

4.1 三个层级:app.use vs app.METHOD vs Router ​

┌─────────────────────────────────────────────────────────────┐
│                Express 路由的三个层级                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Level 1: app.use(path, ...middlewares)                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • 前缀匹配:/api 匹配 /api/users, /api/posts, ... │   │
│  │  • 忽略 HTTP Method(所有方法都进入)                │   │
│  │  • 通常用于:全局中间件、子路由挂载、静态文件        │   │
│  │  app.use("/api", apiRouter)                         │   │
│  │  app.use(express.static("public"))                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 2: app.METHOD(path, ...handlers)                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • 精确匹配:/users/:id 只匹配 /users/123           │   │
│  │  • 限定 HTTP Method:get/post/put/delete/patch      │   │
│  │  • 通常用于:具体的 API 端点                          │   │
│  │  app.get("/users/:id", getUser)                     │   │
│  │  app.post("/users", createUser)                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Level 3: Router() —— 微型 Express 应用                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • 独立的中间件栈和路由表                             │   │
│  │  • 可以挂载到 app 的任意路径前缀下                    │   │
│  │  • 支持自身的 .use() 和 .METHOD()                    │   │
│  │  • 通常用于:按领域拆分路由模块                        │   │
│  │  const userRouter = Router()                        │   │
│  │  userRouter.get("/:id", getUser)                    │   │
│  │  userRouter.post("/", createUser)                   │   │
│  │  app.use("/api/users", userRouter)                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

4.2 路由匹配算法 ​

Express 使用 path-to-regexp 库将路由字符串编译为正则表达式。匹配时按注册顺序线性遍历,找到第一个匹配的路由即执行。

typescript
// 路由字符串 → 正则表达式(path-to-regexp 内部机制)
// "/users/:id"      → /^\/users\/(?:([^\/]+?))\/?$/i    → req.params = { id: "123" }
// "/posts/:postId?" → /^\/posts(?:\/([^\/]+?))?\/?$/i   → id 可选
// "/files/*"        → /^\/files\/(.*)\/?$/i              → 通配符
// "/api/v:version"  → /^\/api\/v(?:([^\/]+?))\/?$/i     → 数字、字母均可

// 示例
import { Router } from "express";

const userRouter = Router();

// 精确匹配
userRouter.get("/profile", getProfile);

// 路径参数
userRouter.get("/:id", getUserById);        // GET /users/123 → req.params.id = "123"

// 可选参数
userRouter.get("/:id/posts/:postId?", getUserPosts); // postId 可选

// 正则约束
userRouter.get("/:id(\\d+)", getUserById);  // id 必须是数字

// 通配符
userRouter.all("*", notFoundHandler);       // 兜底 404

app.use("/users", userRouter);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

4.3 app.route() 链式写法 ​

typescript
// 同一个路径、不同方法的链式定义
app.route("/books/:id")
  .all(authenticate)              // 所有方法都先鉴权
  .get(getBook)                   // GET    /books/123
  .put(updateBook)                // PUT    /books/123
  .delete(deleteBook)             // DELETE /books/123
  .patch(partialUpdateBook);      // PATCH  /books/123

// 等价于传统写法,但更紧凑,意图更清晰
1
2
3
4
5
6
7
8
9

第5部分:常用中间件栈详解 ​

5.1 生产级中间件配置全景 ​

┌─────────────────────────────────────────────────────────────┐
│              生产环境 Express 中间件栈                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  请求进入                                                     │
│     │                                                        │
│     ▼                                                        │
│  ┌──────────────────────────────────────────────────┐       │
│  │ ① helmet       → 设置安全 HTTP 头                 │       │
│  │ ② cors         → 跨域资源共享控制                 │       │
│  │ ③ compression  → gzip/brotli 压缩响应体          │       │
│  │ ④ morgan       → HTTP 访问日志                    │       │
│  │ ⑤ rate-limit   → 请求频率限制                     │       │
│  │ ⑥ express.json → 解析 JSON 请求体                │       │
│  │ ⑦ express.urlencoded → 解析表单请求体             │       │
│  │ ⑧ 自定义中间件  → 认证、日志、验证               │       │
│  │ ⑨ 路由处理      → 业务逻辑                        │       │
│  │ ⑩ 错误处理      → 全局错误兜底                    │       │
│  └──────────────────────────────────────────────────┘       │
│     │                                                        │
│     ▼                                                        │
│  响应返回                                                     │
│                                                             │
│  顺序原则:                                                   │
│  • 安全类中间件最先(helmet, cors)                           │
│  • 日志类尽早(morgan),确保记录所有请求                     │
│  • 压缩类在响应前即可(实际靠 res 事件,位置影响小)          │
│  • 解析类在路由之前(express.json 等)                        │
│  • 限流类在解析之后(可判断 body 大小做分层限流)             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

5.2 cors —— 跨域配置策略 ​

typescript
import cors from "cors";

// 开发环境:宽松配置
app.use(cors({
  origin: "http://localhost:5173",  // Vite 默认端口
  credentials: true,                // 允许携带 Cookie
}));

// 生产环境:白名单策略
const allowedOrigins = [
  "https://myapp.com",
  "https://admin.myapp.com",
];

app.use(cors({
  origin: (origin, callback) => {
    // 非浏览器请求(如 curl、Postman)origin 为 undefined
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("不允许的跨域来源"));
    }
  },
  methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
  exposedHeaders: ["X-Request-Id", "X-RateLimit-Remaining"],
  maxAge: 86400, // 预检请求缓存 24 小时
  credentials: true,
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5.3 helmet —— 安全头设置 ​

typescript
import helmet from "helmet";

// 基础用法(设置一组默认安全头)
app.use(helmet());

// 等价于手动设置以下头部:
// X-DNS-Prefetch-Control: off
// X-Frame-Options: SAMEORIGIN          ← 防止点击劫持
// Strict-Transport-Security: max-age=...← 强制 HTTPS
// X-Download-Options: noopen           ← IE 下载安全
// X-Content-Type-Options: nosniff      ← 防止 MIME 嗅探
// X-Permitted-Cross-Domain-Policies: none
// Referrer-Policy: no-referrer
// X-XSS-Protection: 0                  ← 禁用旧版 XSS 过滤器

// 针对现代应用的精调
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // SPA 可能需要
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      imgSrc: ["'self'", "https://cdn.myapp.com", "data:"],
      connectSrc: ["'self'", "https://api.myapp.com"],
    },
  },
  crossOriginEmbedderPolicy: false, // 如果使用跨域资源
  crossOriginResourcePolicy: { policy: "cross-origin" },
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5.4 compression —— gzip/brotli 压缩 ​

typescript
import compression from "compression";

// 基础配置
app.use(compression({
  // 只压缩大于 1KB 的响应(太小压缩反而增大体积)
  threshold: 1024, // 字节
  // 压缩级别:0(不压缩)~ 9(最高压缩比,最慢)
  level: 6,       // 生产环境推荐的平衡点
  // 对哪些 Content-Type 启用压缩
  filter: (req, res) => {
    if (req.headers["x-no-compression"]) {
      return false; // 允许客户端跳过压缩
    }
    // 使用默认过滤(压缩 text/*, application/json 等)
    return compression.filter(req, res);
  },
}));

// 注意:Nginx / CDN 通常已经处理压缩
// 如果 Express 前置了反向代理,建议将压缩委托给反向代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

5.5 morgan —— 访问日志 ​

typescript
import morgan from "morgan";

// 开发环境:彩色、详细信息
app.use(morgan("dev"));
// 输出:GET /api/users 200 12.345 ms - 1024

// 生产环境:combined 格式(Apache 标准)
app.use(morgan("combined"));
// 输出:127.0.0.1 - - [28/Jul/2026:10:30:00 +0000] "GET / HTTP/1.1" 200 2326

// 自定义 token
morgan.token("request-id", (req: any) => req.requestId || "-");
morgan.token("user-id", (req: any) => req.user?.id || "anonymous");

app.use(morgan(
  ':request-id :remote-addr - :user-id [:date[iso]] ":method :url HTTP/:http-version" :status :res[content-length] - :response-time ms'
));
// 输出:req_abc123 192.168.1.1 - user_456 [2026-07-28T10:30:00+0000] "GET /api/users HTTP/1.1" 200 1024 - 12.345 ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

5.6 express-rate-limit —— 限流 ​

typescript
import rateLimit from "express-rate-limit";

// 全局限流
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟窗口
  max: 100,                  // 每个 IP 最多 100 次请求
  standardHeaders: true,     // 返回 RateLimit-* 头
  legacyHeaders: false,      // 禁用 X-RateLimit-* 头
  message: {
    error: "请求过于频繁,请稍后再试",
    retryAfter: "请等待 15 分钟",
  },
  // 根据请求特征生成 key(默认使用 IP)
  keyGenerator: (req) => {
    // 如果有认证用户,按用户限流更精细
    return req.user?.id ?? req.ip ?? "unknown";
  },
  // 跳过某些请求(如健康检查)
  skip: (req) => req.path === "/health",
});

app.use("/api", globalLimiter);

// 敏感端点专用限流(登录接口)
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10, // 登录接口 15 分钟内只能尝试 10 次
  skipSuccessfulRequests: true, // 登录成功后不计数
});

app.use("/api/auth/login", authLimiter);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

5.7 Body 解析中间件 ​

typescript
// JSON 请求体解析
app.use(express.json({
  limit: "1mb",       // 防止大 payload 攻击
  strict: true,       // 只接受数组和对象,拒绝裸字符串/数字
  verify: (req, _res, buf) => {
    // 可在此验证原始 body 签名(如 Stripe webhook)
    (req as any).rawBody = buf.toString();
  },
}));

// 表单数据解析(application/x-www-form-urlencoded)
app.use(express.urlencoded({
  extended: true,     // 使用 qs 库,支持嵌套对象
  limit: "1mb",
}));

// 注意:express.raw() 和 express.text() 也可用
// 但 multipart/form-data 需要 multer(Express 不内置处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第6部分:自定义中间件设计模式 ​

6.1 认证中间件(JWT 验证) ​

typescript
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

// 扩展 Request 类型(声明合并)
declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
    }
  }
}

export function authenticate(req: Request, res: Response, next: NextFunction): void {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    res.status(401).json({ error: "缺少认证令牌" });
    return;
  }

  const token = authHeader.slice(7); // 去掉 "Bearer " 前缀

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      res.status(401).json({ error: "令牌已过期", code: "TOKEN_EXPIRED" });
      return;
    }
    res.status(401).json({ error: "无效的认证令牌" });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

6.2 请求日志中间件(requestId 注入) ​

typescript
import { randomUUID } from "crypto";
import { Request, Response, NextFunction } from "express";
import { AsyncLocalStorage } from "async_hooks";

// 使用 AsyncLocalStorage 实现请求级上下文
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();

export function requestIdMiddleware(req: Request, _res: Response, next: NextFunction): void {
  // 优先使用传入的 requestId(服务间传递)
  const requestId = (req.headers["x-request-id"] as string) || randomUUID();

  // 注入到 req 对象(供后续中间件使用)
  (req as any).requestId = requestId;

  // 注入到响应头(供客户端追踪)
  _res.setHeader("X-Request-Id", requestId);

  // 注入到 AsyncLocalStorage(使同步流和 microtask 都能访问)
  requestContext.run({ requestId }, () => {
    next();
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

6.3 参数验证中间件(Zod 集成) ​

typescript
import { Request, Response, NextFunction } from "express";
import { z, ZodSchema } from "zod";

// 工厂函数模式:返回可配置的验证中间件
export function validate(schemas: {
  body?: ZodSchema;
  params?: ZodSchema;
  query?: ZodSchema;
}) {
  return (req: Request, res: Response, next: NextFunction): void => {
    try {
      if (schemas.body) {
        req.body = schemas.body.parse(req.body);
      }
      if (schemas.params) {
        req.params = schemas.params.parse(req.params) as any;
      }
      if (schemas.query) {
        // query 的值都是 string,需要特殊处理
        req.query = schemas.query.parse(req.query) as any;
      }
      next();
    } catch (err) {
      if (err instanceof z.ZodError) {
        res.status(400).json({
          error: "请求参数验证失败",
          details: err.errors.map((e) => ({
            path: e.path.join("."),
            message: e.message,
          })),
        });
        return;
      }
      next(err);
    }
  };
}

// 使用示例
const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().int().min(0).max(150).optional(),
  }),
  params: z.object({
    tenantId: z.string().uuid(),
  }),
});

app.post("/api/tenants/:tenantId/users", validate(createUserSchema), createUser);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

6.4 错误处理中间件(4 参数签名) ​

typescript
import { Request, Response, NextFunction } from "express";
import { ZodError } from "zod";

// 自定义错误类
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: unknown
  ) {
    super(message);
    this.name = "AppError";
    // 确保 instanceof 在跨模块场景下正常工作
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// 全局错误处理中间件
export function globalErrorHandler(
  err: Error,
  _req: Request,
  res: Response,
  _next: NextFunction
): void {
  // 已知的业务错误
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      details: err.details,
    });
    return;
  }

  // Zod 验证错误
  if (err instanceof ZodError) {
    res.status(400).json({
      error: "请求参数验证失败",
      code: "VALIDATION_ERROR",
      details: err.errors,
    });
    return;
  }

  // JSON 解析错误(来自 express.json())
  if (err.type === "entity.parse.failed") {
    res.status(400).json({
      error: "请求体格式错误,期望合法的 JSON",
      code: "INVALID_JSON",
    });
    return;
  }

  // 未知错误:记录完整信息,返回泛化消息
  console.error("未处理的错误:", {
    message: err.message,
    stack: err.stack,
    requestId: (res as any).requestId,
  });

  res.status(500).json({
    error: "服务器内部错误",
    code: "INTERNAL_ERROR",
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

6.5 工厂函数模式——可配置的中间件 ​

工厂函数是 Express 中最常见的中间件设计模式:外层函数接收配置,返回符合 (req, res, next) 签名的中间件函数。

┌─────────────────────────────────────────────────────────────┐
│                 中间件工厂函数模式                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  配置参数                                                   │
│     │                                                       │
│     ▼                                                       │
│  ┌──────────────────────────────────────────────────┐       │
│  │ function factory(options) {                        │       │
│  │   // 闭包:options 在中间件生命周期内持久存在       │       │
│  │   return function middleware(req, res, next) {    │       │
│  │     // 每次请求执行的逻辑                          │       │
│  │   }                                               │       │
│  │ }                                                 │       │
│  └──────────────────────────────────────────────────┘       │
│                                                             │
│  典型应用:                                                   │
│  • cors({ origin: "..." })       → 跨域策略可配             │
│  • rateLimit({ windowMs, max })  → 限流参数可配             │
│  • validate({ body: schema })    → 不同 schema 可配         │
│  • authorize("admin")        → 角色可配                   │
│  • cache({ ttl: 60 })        → 缓存时长可配               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript
// 实战:基于角色的授权中间件工厂
export function requireRole(...allowedRoles: string[]) {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (!req.user) {
      res.status(401).json({ error: "未认证" });
      return;
    }
    if (!allowedRoles.includes(req.user.role)) {
      res.status(403).json({ error: "权限不足", required: allowedRoles });
      return;
    }
    next();
  };
}

// 使用
app.delete("/api/users/:id", authenticate, requireRole("admin"), deleteUser);
app.patch("/api/articles/:id", authenticate, requireRole("admin", "editor"), updateArticle);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第7部分:错误处理链 ​

7.1 同步错误 vs 异步错误 ​

┌─────────────────────────────────────────────────────────────┐
│              Express 4 vs Express 5 错误捕获对比              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Express 4:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  throw new Error("sync")   → Express 自动捕获 ✓     │   │
│  │  Promise.reject("async")   → Express 不捕获 ✗       │   │
│  │  async (req, res, next) => {                        │   │
│  │    throw new Error("...")  → Express 不捕获 ✗       │   │
│  │  }                                                   │   │
│  │  → 进程崩溃(unhandledRejection)!                 │   │
│  │  → 必须手动 try/catch + next(err)                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Express 5:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  throw new Error("sync")    → Express 自动捕获 ✓    │   │
│  │  Promise.reject("async")    → Express 自动捕获 ✓    │   │
│  │  async (req, res, next) => {                        │   │
│  │    throw new Error("...")   → Express 自动捕获 ✓    │   │
│  │  }                                                   │   │
│  │  → 所有 rejections 自动传递给 next(err)            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Express 4 的安全写法(向后兼容):                          │
│  const asyncHandler = (fn: RequestHandler) =>               │
│    (req, res, next) => Promise.resolve(fn(req, res, next)) │
│      .catch(next);                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

7.2 Express 4 的 asyncHandler 包装器 ​

typescript
import { Request, Response, NextFunction, RequestHandler } from "express";

// 将 async 路由处理器包装为安全的 Express 中间件
export function asyncHandler(fn: RequestHandler): RequestHandler {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// 使用
app.get("/api/users/:id", asyncHandler(async (req, res) => {
  const user = await db.user.findUnique({ where: { id: req.params.id } });
  if (!user) {
    throw new AppError(404, "NOT_FOUND", "用户不存在"); // 会被 asyncHandler 捕获
  }
  res.json(user);
}));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

7.3 错误响应标准化 ​

typescript
// 统一错误响应格式
interface ErrorResponse {
  error: string;       // 人类可读的错误描述
  code: string;        // 机器可读的错误代码
  details?: unknown;   // 可选的详细错误信息(验证失败字段等)
  requestId?: string;  // 用于关联日志
}

// 全局错误处理器输出标准格式
export function globalErrorHandler(
  err: Error & { statusCode?: number; code?: string; type?: string; details?: unknown },
  req: Request,
  res: Response,
  _next: NextFunction
): void {
  const response: ErrorResponse = {
    error: "服务器内部错误",
    code: "INTERNAL_ERROR",
    requestId: (req as any).requestId,
  };

  if (err instanceof AppError) {
    response.error = err.message;
    response.code = err.code;
    response.details = err.details;
    res.status(err.statusCode).json(response);
    return;
  }

  // 记录未知错误的完整堆栈
  console.error(`[${response.requestId}] 未处理错误:`, err);
  res.status(500).json(response);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第8部分:Express 5 新特性 ​

8.1 Express 5 主要变化 ​

┌─────────────────────────────────────────────────────────────┐
│              Express 4 → Express 5 核心变化                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 自动捕获 async 错误:                                    │
│  async 中间件中抛出的错误自动转到 next(err),无需包装        │
│                                                             │
│  ✅ 路径匹配语法更新:                                       │
│  • 废弃正则表达式路径(app.get(/regex/, handler) 不再支持) │
│  • 路径参数支持更多修饰符:/:id(\\d+), /:name*, /:file?     │
│  • 使用 path-to-regexp 0.x(语法有变化)                    │
│                                                             │
│  ✅ 废弃的方法和属性:                                       │
│  • req.query 不再可写(Express 4 中可手动设值)             │
│  • req.param() 被废弃(用 req.params / req.body / req.query │
│  • res.send(status, body) 签名被废弃(用 res.status().send() │
│  • app.del() 被废弃(用 app.delete())                      │
│  • app.param() 回调被废弃                                   │
│                                                             │
│  ✅ 更好的 Promise 支持:                                    │
│  • app.param() 处理器支持返回 Promise                       │
│  • 模板引擎 render() 支持返回 Promise                       │
│                                                             │
│  ⚠️ 迁移注意事项:                                          │
│  • 检查所有路由中的正则表达式用法                            │
│  • 检查 app.param(fn) 回调                                   │
│  • 检查 res.send(status, body) 调用                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typescript
// Express 5 路径匹配新语法
const router = Router();

// 命名参数 + 正则约束(与 Express 4 一致,但底层引擎升级)
router.get("/users/:id(\\d+)", handler);

// 零个或多个(*)
router.get("/files/:path*", handler);    // /files, /files/a, /files/a/b/c

// 一个或多个(+)
router.get("/sections/:name+", handler); // /sections/a, /sections/a/b

// 可选参数(?)
router.get("/posts/:slug?", handler);    // /posts, /posts/hello-world
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第9部分:Express 性能调优 ​

9.1 中间件顺序优化 ​

┌─────────────────────────────────────────────────────────────┐
│              中间件顺序对性能的影响                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  黄金法则:                                                  │
│  1. 尽早短路不必要的请求                                      │
│  2. 让开销大的中间件处理尽可能少的请求                        │
│  3. 静态文件放在 API 路由之前或之后                          │
│                                                             │
│  反例(每次 API 请求都经过静态文件查找):                    │
│  app.use(express.static("public"))  // public 目录无匹配     │
│  app.use("/api", apiRouter)         // 才到 API 路由        │
│  → 每个 API 请求都对 public 目录做 fs.stat                  │
│                                                             │
│  正例(API 请求跳过静态文件中间件):                         │
│  app.use("/api", apiRouter)         // API 请求直接匹配     │
│  app.use(express.static("public"))  // 只有非 /api 请求到达  │
│                                                             │
│  推荐的全局中间件顺序:                                      │
│  ┌──────────────────────────────────────────────────┐       │
│  │① 安全类(helmet, cors)                           │       │
│  │② 请求日志(morgan, requestId)                    │       │
│  │③ Body 解析(express.json, urlencoded)           │       │
│  │④ 压缩(compression)                              │       │
│  │⑤ 限流(rateLimit)                                │       │
│  │⑥ 认证(authenticate, 按需)                      │       │
│  │⑦ API 路由                                         │       │
│  │⑧ 静态文件(如果 /api 路由已分开)                │       │
│  │⑨ 404 兜底                                         │       │
│  │⑩ 全局错误处理                                     │       │
│  └──────────────────────────────────────────────────┘       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

9.2 生产环境核心配置 ​

typescript
import express from "express";

const app = express();

// 1. 信任反向代理(获取真实客户端 IP)
app.set("trust proxy", 1); // 信任第一个代理
// app.set("trust proxy", ["127.0.0.1", "10.0.0.0/8"]); // 信任特定 IP

// 2. 关闭 Express 签名头(减少响应体积)
app.disable("x-powered-by");

// 3. 设置合理的超时
import http from "http";
const server = http.createServer(app);
server.timeout = 30_000;     // 30 秒不活动超时
server.keepAliveTimeout = 65_000; // Keep-Alive 超时
server.headersTimeout = 66_000;   // 头部解析超时(应 > keepAliveTimeout)

// 4. 优雅关闭
process.on("SIGTERM", () => {
  console.log("收到 SIGTERM,开始优雅关闭...");
  server.close(() => {
    console.log("HTTP 服务器已关闭");
    // 关闭数据库连接、消息队列连接等
    process.exit(0);
  });

  // 强制退出兜底
  setTimeout(() => {
    console.error("优雅关闭超时,强制退出");
    process.exit(1);
  }, 10_000);
});

// 5. 处理未捕获的异常和 rejection
process.on("uncaughtException", (err) => {
  console.error("未捕获异常:", err);
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  console.error("未处理的 Promise rejection:", reason);
  // Express 5 已自动处理,此处理仅用于非 Express 代码的兜底
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

9.3 常见性能陷阱 ​

typescript
// ❌ 反模式1:在中间件中同步执行密集计算
app.use((req, res, next) => {
  const result = heavyCryptoOperation(req.body); // 阻塞事件循环!
  next();
});

// ✅ 正解:将 CPU 密集操作放到 Worker Thread 或独立服务
app.use(asyncHandler(async (req, res, next) => {
  const result = await workerPool.exec("heavyCrypto", [req.body]);
  next();
}));

// ❌ 反模式2:不设置 Body Parser 的 limit
app.use(express.json()); // 攻击者可以发送无限大的 JSON,OOM

// ✅ 正解:设定合理的上限
app.use(express.json({ limit: "1mb" }));

// ❌ 反模式3:在同步中间件中 await
app.use((req, res, next) => {
  // 忘了把回调转成 await,next 在等待前就已经调用
  someAsyncCheck(req).then(() => next());
  // 如果 someAsyncCheck 抛出异常 → unhandledRejection
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

第10部分:Express + TypeScript 最佳实践 ​

10.1 类型化 Request / Response ​

typescript
import { Request, Response, NextFunction } from "express";

// 1. 为路由参数定义类型
interface UserParams {
  id: string;
}

interface UserQuery {
  page?: string;
  limit?: string;
  sort?: "asc" | "desc";
}

interface CreateUserBody {
  name: string;
  email: string;
  age?: number;
}

// 使用泛型约束
app.get<{ id: string }>(
  "/api/users/:id",
  (req: Request<UserParams>, res: Response) => {
    const { id } = req.params; // id 的类型是 string
    // ...
  }
);

// 更完整的泛型用法
app.post<{}, any, CreateUserBody>(
  "/api/users",
  (req: Request<{}, any, CreateUserBody>, res: Response) => {
    const { name, email } = req.body; // ✅ 类型安全
    // ...
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

10.2 扩展 Request 类型(声明合并) ​

typescript
// types/express.d.ts
import "express";

declare global {
  namespace Express {
    interface Request {
      // 认证后注入的用户信息
      user?: {
        id: string;
        role: "admin" | "user" | "editor";
        tenantId: string;
      };

      // 每个请求的唯一 ID
      requestId: string;

      // 多租户上下文
      tenant?: {
        id: string;
        name: string;
      };

      // 请求开始时间(用于计算耗时)
      startTime: number;
    }
  }
}

// 在任何地方使用都有完整的类型提示
app.get("/profile", authenticate, (req, res) => {
  req.user; // { id: string; role: "admin" | "user" | "editor"; tenantId: string }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

10.3 Zod 验证 + 类型推导形成端到端类型安全 ​

typescript
import { z } from "zod";

// 1. 定义 Zod Schema(既是验证器,又是类型来源)
const createUserSchema = z.object({
  name: z.string().min(2, "姓名至少 2 个字符").max(50),
  email: z.string().email("邮箱格式不正确"),
  role: z.enum(["user", "editor"]).default("user"),
});

const updateUserSchema = createUserSchema.partial(); // 所有字段变为可选

const userIdParamsSchema = z.object({
  id: z.string().regex(/^\d+$/, "ID 必须是数字").transform(Number),
});

// 2. 从 Schema 推导 TypeScript 类型(单一真相源)
type CreateUserInput = z.infer<typeof createUserSchema>;
type UpdateUserInput = z.infer<typeof updateUserSchema>;
type UserIdParams = z.infer<typeof userIdParamsSchema>;

// 3. 泛型验证中间件(支持类型推导)
import { ZodSchema, ZodTypeAny } from "zod";

interface ValidationSchemas<
  TParams extends ZodTypeAny = ZodTypeAny,
  TBody extends ZodTypeAny = ZodTypeAny,
  TQuery extends ZodTypeAny = ZodTypeAny,
> {
  params?: TParams;
  body?: TBody;
  query?: TQuery;
}

export function validate<
  TParams extends ZodTypeAny,
  TBody extends ZodTypeAny,
  TQuery extends ZodTypeAny,
>(schemas: ValidationSchemas<TParams, TBody, TQuery>) {
  return (req: Request, res: Response, next: NextFunction): void => {
    try {
      if (schemas.body) req.body = schemas.body.parse(req.body);
      if (schemas.params) (req as any).params = schemas.params.parse(req.params);
      if (schemas.query) (req as any).query = schemas.query.parse(req.query);
      next();
    } catch (err) {
      next(err);
    }
  };
}

// 4. 使用:Schema → 验证 → 类型自动匹配
app.post(
  "/api/users",
  validate({ body: createUserSchema }),
  (req, res) => {
    // req.body 的类型自动推导为 CreateUserInput
    const { name, email, role } = req.body; // ✅ 完全类型安全
    res.status(201).json({ name, email, role });
  }
);

app.patch(
  "/api/users/:id",
  validate({ params: userIdParamsSchema, body: updateUserSchema }),
  (req, res) => {
    // req.params.id 的类型是 number(因为 .transform(Number))
    const { id } = req.params; // number!
    const updates = req.body;  // UpdateUserInput
    res.json({ id, updates });
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

10.4 项目结构推荐 ​

src/
├── index.ts              # 入口:创建 app,注册中间件,启动 server
├── app.ts                # Express 应用工厂(便于测试)
├── config/
│   └── env.ts            # 环境变量验证(Zod 验证 process.env)
├── middleware/
│   ├── authenticate.ts   # JWT 认证中间件
│   ├── authorize.ts      # 角色授权中间件(工厂函数)
│   ├── requestId.ts      # requestId 注入
│   ├── validate.ts       # Zod 验证中间件
│   └── errorHandler.ts   # 全局错误处理器
├── routes/
│   ├── index.ts          # 路由聚合(挂载所有子路由到 /api)
│   ├── users.ts          # /api/users 路由
│   ├── articles.ts       # /api/articles 路由
│   └── auth.ts           # /api/auth 路由
├── services/             # 业务逻辑层(不依赖 req/res)
│   ├── userService.ts
│   └── articleService.ts
├── types/
│   └── express.d.ts      # Request 声明合并
└── utils/
    ├── AppError.ts       # 自定义错误类
    └── asyncHandler.ts   # Express 4 兼容包装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

核心总结 ​

  1. 洋葱模型:Express 的核心是中间件调度,next() 将控制权交给下一层,后置逻辑在 next() 之后执行,形成请求「进→出」的洋葱环。
  2. next(err) 短路:传递错误会使 Express 跳过所有普通中间件,直接寻找 4 参数签名的错误处理器。
  3. 三层层级:app.use(前缀匹配,任意方法)> app.METHOD(精确匹配,特定方法)> Router(独立子应用,支持模块化)。
  4. 中间件顺序:安全类(helmet/cors)> 日志类(morgan)> 解析类(body parser)> 限流类 > 认证类 > 路由 > 错误处理。顺序决定性能和安全性。
  5. 工厂函数模式:外层接收配置,内层返回中间件函数,是 Express 中最通用的可复用模式。
  6. Express 5:自动捕获 async 错误的 Promise rejection,移除了路径正则表达式支持,语法更清晰。
  7. TypeScript 集成:通过声明合并在 Express.Request 上添加属性,Zod Schema 既是验证器又是类型来源,实现端到端类型安全。

章节测试 ​

  1. 洋葱模型:在中间件 A (前置) → 中间件 B (前置) → handler → 中间件 B (后置) → 中间件 A (后置) 的执行链条中,若中间件 B 的前置逻辑调用了 next(new Error("fail")),请写出实际执行路径。

  2. 路由匹配:app.use("/api", router) 和 app.get("/api/users", handler) 分别属于哪个层级?它们的匹配方式有什么区别?

  3. 错误处理:Express 4 中为什么 async (req, res) => { throw new Error("fail") } 不会触发错误处理中间件?Express 5 如何解决这个问题?

  4. 中间件设计:请写一个工厂函数中间件,它接收 rateLimitOptions 配置,返回一个限流中间件(伪代码即可)。

  5. 声明合并:如何通过 TypeScript 的声明合并为 Express.Request 添加 user 和 requestId 属性?

  6. 性能优化:为什么静态文件中间件 express.static("public") 应该放在 API 路由之后?这样做的原理是什么?

  7. Body 解析:express.json({ limit: "1mb" }) 中的 limit 参数的作用是什么?为什么不设置它会带来安全风险?


参考答案 ​

  1. 执行路径:A 前置 → B 前置 → next(new Error("fail")) → 跳过 handler、B 后置、A 后置 → 直接进入第一个 (err, req, res, next) 签名的错误处理器。

  2. app.use("/api", router) 是 Level 1(前缀匹配),任何以 /api 开头的请求(不限 HTTP 方法)都会匹配并进入 router。app.get("/api/users", handler) 是 Level 2(精确路由),只有 GET /api/users 精确匹配时才执行,其他方法(POST、PUT 等)不会命中。

  3. Express 4 的中间件调度器不会自动捕获 Promise rejection。async 函数抛出错误本质是返回一个 rejected Promise,Express 4 没有 .catch() 机制来处理它,因此会变成 unhandledRejection。Express 5 在调度器中添加了对 async 函数返回值的 .catch(next) 处理,自动将 rejection 转入错误处理链。

  4. 伪代码:

typescript
function createRateLimiter(options: { windowMs: number; max: number }) {
  const hits = new Map<string, { count: number; resetAt: number }>();
  return (req: Request, res: Response, next: NextFunction) => {
    const key = req.ip ?? "unknown";
    const now = Date.now();
    const record = hits.get(key);
    if (!record || now > record.resetAt) {
      hits.set(key, { count: 1, resetAt: now + options.windowMs });
      return next();
    }
    if (record.count >= options.max) {
      return res.status(429).json({ error: "请求过于频繁" });
    }
    record.count++;
    next();
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 在 types/express.d.ts 中:
typescript
declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
      requestId: string;
    }
  }
}
1
2
3
4
5
6
7
8
  1. 静态文件中间件对每个请求都会执行文件系统查找(fs.stat)。如果放在 API 路由之前,每个 API 请求(如 /api/users)都会先经历一次无意义的文件查找,造成不必要的 I/O 开销。放在 API 路由之后,API 请求会先被路由精确匹配并处理,不再进入静态文件中间件。

  2. limit 限制请求体的最大大小(字节数)。不设置意味着攻击者可以发送任意大小的 JSON,导致服务端内存耗尽(OOM),造成拒绝服务攻击。1mb 是常见的安全上限。


相关笔记 ​

  • [[../01-javascript-and-typescript/03-async-and-event-loop]] — Node.js 事件循环与异步模型
  • [[00-overview]] — Node.js 与后端框架学习路线
  • [[01-environment-configuration]] — 环境配置完全指南
  • [[../../05-backend-engineering/11-architecture-patterns]] — 后端架构模式
  • [[../../05-backend-engineering/18-observability]] — 可观测性(日志、追踪、指标)

下一步学习 ​

  • Express 官方文档:深入了解 app.set() 配置项和模板引擎集成
  • Fastify 快速入门:对比 Express 的插件系统和 Schema 验证,理解性能差异来源
  • NestJS 基础:体验装饰器 + 依赖注入的全栈架构
  • 阅读 express 源码(~2000 行):理解 Layer、Route、Router 的内部实现
  • 尝试徒手实现一个最小 Express(洋葱模型 + 路由匹配)

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇3. Node.js 异步模式与错误处理 / Async Patterns and Error Handling in Node.js
下一篇5. Express 高级模式与生产实践 / Express Advanced Patterns and Production Practices

持续记录,持续成长

Copyright © Tidenflow