API 路由命名规范:如何避免静态路径被误识别为 id?
先处理静态路由,再限制 id 的取值
/tasks/archived-count 这个名称可以保留。要避免它匹配到 :id,需要根据框架的路由匹配规则处理:
- 框架按注册顺序匹配时,把
/tasks/archived-count放在/tasks/:id前面。 - id 有固定格式时,例如纯数字或 UUID,按实际格式约束参数,并在查询数据前校验。
- id 允许任意字符串时,约定保留路径名称,或把统计接口与按 id 查询的接口分开。
问题中没有说明使用的框架,因此不能认定静态路由一定优先。下面以 Express 为例。
为什么 archived-count 会匹配到 :id?
没有额外约束时,:id 通常只表示捕获这个位置的一个路径片段。路由器不会因为参数名叫 id,就自动判断它必须是数字、UUID,或数据库中的有效标识符。
因此,下面两个请求都可能匹配 /tasks/:id:
GET /api/v1/tasks/123
GET /api/v1/tasks/archived-count
第二个请求传给动态路由的参数是:
req.params.id === 'archived-count'
在 Express 中,如果动态路由先注册,并直接结束响应,后面的静态路由就没有机会处理请求。
在 Express 中如何配置?
先注册静态路由,再注册动态路由。以下示例假设 task id 只允许 ASCII 数字,处理器由业务代码提供。
const express = require('express');
const {
handleArchivedCount,
handleTaskById,
} = require('./task-handlers');
const app = express();
// 静态路由先注册
app.get(
'/api/v1/tasks/archived-count',
handleArchivedCount
);
// 动态路由随后注册,并在查询前校验参数
app.get(
'/api/v1/tasks/:id',
(req, res, next) => {
if (!/^[0-9]+$/.test(req.params.id)) {
return res.status(400).json({
error: 'Invalid task id',
});
}
next();
},
handleTaskById
);
注册顺序和参数校验各有作用:
- 注册顺序让
archived-count进入统计处理器。 - 参数校验让其他格式错误的 id 在查询数据前返回错误。
如果动态路由放在前面,并在校验失败时直接返回 400,请求仍然到不了后面的静态路由。所以,仅增加校验不能替代正确的路由配置。
实际使用 UUID 时,应按项目接受的 UUID 格式校验。原本合法的字符串 id 也应继续支持,不要为了消除冲突临时改成数字。
能否直接在路由中限制参数格式?
这取决于框架及其版本支持的语法。
有些路由器支持参数类型、转换器或正则约束。如果在路由匹配阶段约束参数,不符合格式的片段就不会进入该动态路由。Express 不同版本对路径模式的支持有差异,不能直接套用其他版本的正则写法。
路由约束和处理器校验也需要区分:
- 路由约束决定请求是否匹配该路由。
- 处理器中的校验决定匹配之后是否接受该参数。
两者执行的位置不同,失败后的响应也可能不同。
是否需要修改 API 命名?
如果只有一个归档计数接口,可以保留现有设计:
GET /api/v1/tasks/:id
GET /api/v1/tasks/archived-count
如果后续还会增加按状态统计的接口,可以考虑统一为:
GET /api/v1/tasks/count?status=archived
status=archived 表示统计条件。不过,count 仍然可能匹配 :id,依然需要处理路由优先级或参数约束。查询参数不会自动消除路径冲突。
也可以为统计接口增加一层路径:
GET /api/v1/tasks/stats/archived-count
GET /api/v1/tasks/:id
在完整路径匹配、且 :id 只匹配单个片段的前提下,统计路径多了一层,不会匹配 /tasks/:id。如果项目中有前缀匹配中间件或通配路由,还要检查它们是否会提前拦截请求。
id 可以是任意字符串时怎么办?
如果 archived-count 本身也是合法的 task id,同一个 HTTP 方法和路径就有了两种含义:
GET /api/v1/tasks/archived-count
路由器无法判断调用方想获取任务详情,还是归档任务数量。调整注册顺序只能让其中一种含义优先,无法同时支持两者。
这时需要选择一种方案:
- 将
archived-count等静态名称设为保留名称,在创建、修改 id 时禁止使用。 - 使用独立路径,例如:
GET /api/v1/tasks/by-id/:id
GET /api/v1/tasks/archived-count
第二种方案可以让任意字符串 id 与统计路径分开,但修改已有接口时需要考虑客户端兼容性。
上线前需要检查什么?
至少验证以下请求:
| 请求 | 预期行为 |
|---|---|
/tasks/archived-count | 进入归档计数处理器 |
/tasks/123 | 在数字 id 的前提下进入详情处理器 |
/tasks/abc | 按数字 id 的校验规则返回参数错误 |
| 格式合法但不存在的 id | 详情接口返回 404 |
还要检查路由挂载顺序、参数中间件和通配路由。框架若支持明确的路由优先级,应按其规则配置;不能只凭路径看起来更具体,就认定它会先匹配。
备注:内容仅供参考。
