Express 路由怎么讲:匹配机制、参数处理、模块拆分与 REST 设计
面试速答(30 秒版)
- Express 路由的核心作用是:根据请求方法和路径,把请求分发到对应的处理函数。
- 路由不只是
app.get("/xxx")这么简单,它背后包含 路径匹配、参数提取、中间件挂载、模块拆分。 - 面试里真正需要讲清楚的是:路由怎么组织、为什么要分层、怎样避免写成巨型路由文件。
- 对中大型项目,常见做法是
Router分模块,再配合控制器、服务层和中间件解耦。 - 一句话总结:Express 路由本质上是“请求分发规则 + 局部中间件链”的组合。
一、先建立最基础的理解
一个最小的 Express 路由:
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
这段代码背后干了三件事:
- 限定只处理
GET请求。 - 只匹配
/users/:id这种路径。 - 把路径参数解析到
req.params,然后执行 handler。
如果面试官继续问“路由是什么”,比起只说“定义接口地址”,更完整的表达是:
- 路由负责把 HTTP 方法、URL 结构和业务处理函数关联起来。
二、路由匹配机制怎么理解
1. 匹配维度有两个
- HTTP 方法
- 路径模式
例如:
app.get("/users", listUsers);
app.post("/users", createUser);
虽然路径一样,但方法不同,所以是两条不同路由。
2. 路径参数会被提取
app.get("/orders/:orderId/items/:itemId", (req, res) => {
res.json(req.params);
});
访问 /orders/10/items/2 时:
req.params; // { orderId: "10", itemId: "2" }
3. 查询参数不属于路由路径本身
比如请求:
/users?page=2&size=20
这里:
/users是路由匹配部分page、size在req.query里
这也是面试常考点,很多人会把 params 和 query 混掉。
三、Express 路由为什么建议模块化
小项目里,直接在 app.js 写所有路由还勉强能看。
一旦接口多起来,就会出现这些问题:
- 单文件几百行甚至上千行
- 用户、订单、支付逻辑混在一起
- 中间件重复挂载
- 测试边界不清晰
所以更合理的方式是按领域拆 Router。
例如:
const express = require("express");
const userRouter = express.Router();
userRouter.get("/", listUsers);
userRouter.get("/:id", getUserById);
userRouter.post("/", createUser);
module.exports = userRouter;
主应用再挂载:
app.use("/users", userRouter);
这样带来的好处:
- 路由职责清晰
- 中间件可以局部挂载
- 更适合团队协作
四、路由和中间件怎么配合
路由不是只能挂一个 handler。
router.get(
"/profile",
authMiddleware,
permissionMiddleware("user:read"),
getProfile
);
这表示在进入最终业务处理前,先过一段局部中间件链。
你可以把它理解成:
这种设计的核心价值是:
- 公共能力复用,但作用域只限制在相关路由组。
五、路由设计要不要遵循 REST
面试里常会带到这个问题。
更稳的回答不是“必须 RESTful”,而是:
- REST 是常见约定,目的是让 URL 和方法语义更统一。
- 但工程里优先级更高的是可维护性、一致性和团队规范。
典型例子:
| 操作 | 更常见的 REST 风格 |
|---|---|
| 查询用户列表 | GET /users |
| 查询用户详情 | GET /users/:id |
| 创建用户 | POST /users |
| 更新用户 | PUT /users/:id 或 PATCH /users/:id |
| 删除用户 | DELETE /users/:id |
面试补充点:
PUT更偏整体替换。PATCH更偏局部更新。- 不是所有业务动作都能优雅映射成资源操作,比如“审核通过”“发券”“重试任务”这种命令式动作。
六、一个更像工程项目的路由分层
在中大型 Express 服务里,常见结构不是“路由里把所有逻辑写完”,而是拆成:
router:只负责 URL 到 handler 的映射。controller:处理请求输入输出,协调服务。service:承载业务逻辑。repository/model:访问数据库。
例如:
router -> controller -> service -> db
为什么这样拆:
- 路由层应该尽量薄。
- 否则后面所有业务都会黏在 Express API 上,不利于测试和复用。
七、参数校验应该放哪
这是很高频的追问。
推荐思路:
- 路由负责声明入口。
- 参数校验放在路由级中间件或 controller 入口。
- service 假设拿到的是“基本合法的数据”。
不要把校验散落到每一层,否则会出现:
- 重复校验
- 状态码不一致
- 错误信息风格混乱
八、404 路由和兜底怎么处理
如果没有命中任何路由,通常要有统一 404 处理:
app.use((req, res) => {
res.status(404).json({ message: "Not Found" });
});
它要放在所有路由之后。
之后再接错误处理中间件。
这背后的思路是:
- 先判断有没有匹配路由。
- 没有命中时返回 404。
- 如果命中了但执行报错,再由错误处理中间件兜底。
典型题与标准答法
1. Express 路由是什么?
- 是基于 HTTP 方法和路径规则的请求分发机制。
- 匹配成功后执行对应的处理函数和局部中间件链。
2. req.params 和 req.query 有什么区别?
params来自路径占位符。query来自 URL 查询字符串。
3. 为什么要用 express.Router()?
- 为了模块化路由。
- 便于按业务域拆分、局部挂载中间件和降低主入口复杂度。
4. 路由层为什么不能太重?
- 因为它应该只关心请求分发和输入输出边界。
- 业务逻辑都堆在路由层会导致耦合严重、测试困难。
常见追问
1. 一个路由文件该怎么拆?
- 常见是按资源域拆,比如
user、order、auth。 - 不建议按 HTTP 方法拆。
2. URL 一定要完全 RESTful 吗?
- 不一定。
- 但至少要保持风格统一,避免一部分像资源,一部分像 RPC,最后没人看得懂。
3. 版本路由怎么做?
- 可以挂前缀,比如
/api/v1/users。 - 也可以在网关层做版本隔离。
易错点
- 把
params和query混为一谈。 - 路由文件里直接写大量数据库和业务逻辑。
- 所有接口都堆在一个
app.js。 - 没有统一 404 和错误兜底。
- REST 只背 URL 形式,不理解资源语义和工程妥协。
速记要点
- 路由 = 方法匹配 + 路径匹配 + 处理链分发。
params来自路径,query来自查询串。Router的价值是模块化和局部中间件。- 路由层要薄,业务逻辑下沉到 controller / service。
- 404 处理放路由后,错误处理中间件放最后。