识别与构造:在 Express 中选择路由参数还是查询字符串
了解何时应将值放在 Express 路由参数中而非查询字符串中,如何读取 req.params 和 req.query,以及如何安全地处理默认值和数据类型。
以 URL /users/42?sort=name&order=asc 为例。42 用于指定某个特定的用户;而 sort=name&order=asc 只是改变响应的排列方式。如果将这两者的作用混淆,比如把 ID 当作筛选条件,或者把筛选条件当作 ID,就会导致 Express 路由出现问题。读完本指南后,你将拥有一个简单的测试方法来判断某个值应属于何处,同时也会了解 Express 是如何处理不同类型的参数以及可能出现的意外情况。
路由参数用于标识资源
路由参数是路径模式中带有名称的组成部分,它告诉服务器该请求涉及的是哪一种具体的资源。
/users/:id
冒号将:id标记为占位符。当收到对/users/42的请求时,Express会匹配该模式,并将42记录为id的值。这一原理同样适用于任何具有标识符的资源:
/users/42 → which user
/products/17 → which product
/orders/1042 → which order
这些路径名各自仅对应一个资源。如果没有该路径段,就无从确定“是哪一个?”的问题。
查询字符串决定响应内容
查询字符串指的是?之后的所有内容:由&连接的一组key=value对。它并不用于选择资源,而是用来过滤、排序、分页或以其他方式调整返回的内容。
/users?sort=name&order=asc
在这里,sort和order不会改变资源(即用户集合)本身,仅影响其显示方式。更典型的例子如下:
/products?category=electronics&maxPrice=500
/search?q=laptop&page=2
区分两者的一个测试问题
询问删除该值后会发生什么:
- 如果路由不再有意义(不指定具体用户就无法获取“某个用户”),则该值属于标识符,应放在路径中。
- 如果路由仍然正常工作,只是返回默认的、未过滤的结果(按标准顺序列出所有用户),则该值属于修饰符,应放在查询字符串中。
这个测试也能帮助理解错误处理方式。路由参数对应的资源不存在时通常应返回404状态码,而无法匹配任何条件的过滤器则应返回200状态码并附带空列表。如需更全面地了解以资源为导向的URL设计,可参阅面向初学者的REST API指南。
使用req.params读取路由参数
在路由路径中,参数通过冒号声明,捕获到的值会以相同的名称出现在 req.params 中:
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.send(`Fetching user with ID: ${userId}`);
});
对 /users/42 的请求会将 req.params.id 设置为字符串 "42"。
一个路径中包含多个参数
嵌套资源只需声明更多占位符即可:
app.get("/users/:userId/orders/:orderId", (req, res) => {
const { userId, orderId } = req.params;
res.send(`User ${userId}, Order ${orderId}`);
});
对于 /users/42/orders/1042,解构后会得到 userId 的值为 "42",orderId 的值为 "1042"。参数名称在同一个路由中必须是唯一的,且应能体现其所标识的内容;使用 userId 和 orderId 比两个匿名的 id 更易于理解。
使用 req.query 读取查询字符串
在路由中无需声明查询参数值。Express会解析?之后的内容并将其放入req.query中:
app.get("/users", (req, res) => {
const { sort, order } = req.query;
res.send(`Sorting by ${sort}, order: ${order}`);
});
对于/users?sort=name&order=asc,此时req.query.sort的值为"name",req.query.order的值为"asc"。路由本身仍为/users,因此同一个处理函数可以同时处理普通请求和已排序的请求。
为可选参数提供默认值
由于客户端常常不填写查询参数,处理函数通常会使用合理的默认值作为替代:
app.get("/products", (req, res) => {
const sort = req.query.sort || "default";
const page = req.query.page || 1;
res.send(`Sorting: ${sort}, Page: ${page}`);
});
仅发送 /products 请求时仍会因使用默认值而成功。需要注意一个细节:当客户端确实发送了 page=2 时,page 的值为字符串 "2";而当未指定该参数时,其值为数字 1。此类混合类型日后会导致错误(例如将字符串进行连接而非相加)。应显式进行类型转换,如使用 Number(req.query.page) || 1,并在将其用于数据库查询之前验证结果。
值并不总是单一字符串
URL中重复出现的键,如?tag=a&tag=b,会以数组形式而非字符串形式出现。根据查询解析器的设置,括号语法还可能生成嵌套对象。Express 5将默认解析器更改为比Express 4更简单的版本,因此如果需要使用嵌套查询对象,请查阅对应版本的文档。无论如何,都不要假设查询值的类型;应将req.query视为不可信的输入。
确定路由需要哪种类型
特定资源的路径参数
app.get("/users/:id", ...) // one specific user
app.get("/products/:id", ...) // one specific product
app.get("/orders/:orderId", ...) // one specific order
这些路由每一个都对应一个具体的项目。如果缺少该值路由就无意义,那么应将其放入路径中。
用于过滤、排序和分页的查询字符串
app.get("/users", ...) // ?role=admin&status=active
app.get("/products", ...) // ?category=electronics&maxPrice=500&sort=price
app.get("/search", ...) // ?q=laptop&page=2
即便不使用查询条件,这些表达依然有意义:“所有用户”、“所有产品”,或是空搜索页面。那些仅用于缩小结果范围或重新排序的值属于可选的修饰符,应放在?之后。
在单个路由中同时使用两者
实际的接口端点常常会同时使用这两种方式。参数用于指定所有者,而查询条件则用于缩小相关数据的范围:
app.get("/users/:id/orders", (req, res) => {
const userId = req.params.id; // which user
const status = req.query.status; // optional filter: only their pending orders, for example
res.send(`Orders for user ${userId}, filtered by status: ${status || "all"}`);
});
对/users/42/orders?status=pending的请求很容易理解:42表示属于哪位用户的订单,而status=pending则表示要包含哪些状态的订单。当没有指定status时,处理程序会返回“全部”,这符合缺失修饰符即表示不加过滤的默认逻辑。
常见问题
一个路由能否同时使用两者?
是的,这种情况非常常见。上面的综合示例就是典型模式:父资源的标识符加上其子资源的可选过滤器。
参数一定是必需的,而查询值一定是可选的吗?
这只是一个普遍惯例,并非硬性规定。Express确实支持可选的路径片段,也没有什么能阻止API要求提供查询值。不过实际操作中仍遵循这一准则:必需的标识符放在路径中,可选的修饰符则带默认值放在查询参数中。
req.params.id是数字吗?
不是。从URL中提取的所有内容都是字符串,即便看起来像数字也是如此。应在使用之前显式进行转换,例如通过Number(req.params.id),并在接触数据库之前排除返回NaN的值。
如果预期的查询值缺失了怎么办?
req.query 中根本不存在该键,因此读取时会得到 undefined。这正是之前所展示的默认值作为备选方案之所以成为标准做法的原因。
总结
这两种类型的值都存在于同一个 URL 中,但功能各异。从 req.params 读取的路由参数用于标识请求所涉及的具体内容;而从 req.query 读取的查询字符串则用于控制响应的过滤、排序或分页方式,且应设置默认值。应将它们视为来自外部的未类型化字符串:在使用前先进行转换和验证,这样随着 API 的发展,你的路由设计依然能够保持可预测性。
相关阅读
- 守护 Express 边界:一个用于处理请求体、参数和查询字符串的 Zod 中间件 — 了解如何使用一个可重用的 Zod 中间件来验证 Express 请求体、路由参数和查询字符串,以及它如何与 Sequelize 模型验证相辅相成。
- 面向初学者的 REST API:资源、方法、状态码与无状态性 — 用通俗的语言讲解 REST API 是什么,使其正常运行的五大核心原理,它在实际团队中的应用场景,以及如何构建和测试你的第一个 REST API。