Express中req.params、req.query与req.body参数解析原理

发布时间:2026/6/21 14:17:22
Express中req.params、req.query与req.body参数解析原理 1. 项目概述为什么搞懂 Express 中的 URL 与 POST 参数是后端开发的“呼吸本能”刚入行那会儿我写第一个登录接口前端传了用户名和密码我在后端死活拿不到——req.body是空的req.query里没有req.params也查无此人。调试半小时最后发现连body-parser都没装更别说配置了。这种“明明代码看着没错但数据就是不进来”的挫败感几乎每个用 Express 写过真实接口的人都踩过。今天这篇不是教你怎么复制粘贴一个 demo而是带你把 Express 处理请求参数的整条链路从网络层、框架层到应用层一层层剥开来看清。核心关键词Express、URL parameters、POST parameters、req.query、req.params它们不是孤立的 API 文档词条而是你每天和 HTTP 请求打交道时最常触碰的三个“开关”。req.query对应浏览器地址栏里?keyvaluefoobar这种显性参数是 GET 请求的标配req.params是路由路径里的动态占位符比如/user/:id中的:id它决定了你的 API 路径设计是否 RESTful而req.body常被泛称为 POST 参数才是真正的“数据载体”承载着表单提交、JSON 接口、文件上传等所有需要传递结构化数据的场景。这三者在 Express 中的处理机制完全不同query和params由框架在解析 URL 时自动提取无需额外中间件但body的解析则完全依赖express.json()和express.urlencoded()这两个中间件它们就像两道安检门缺一不可。很多人卡在req.body为空根本原因不是代码写错了而是这两道门根本没打开。这篇文章我会用一个真实电商后台的订单查询接口作为贯穿案例——它同时需要req.params获取订单 ID、req.query支持分页和状态筛选、req.body接收修改订单状态的 JSON 数据——手把手带你把每一步的原理、配置、调试方法和典型陷阱都拆解清楚。无论你是刚学完 Node.js 基础想上手实战还是已经写了半年接口却总在参数问题上反复调试的老手这篇内容都能让你下次遇到undefined时不再靠猜而是能立刻定位到是哪一层出了问题。2. 核心机制拆解Express 如何像拆快递一样解析 HTTP 请求2.1 三层参数的本质区别不是 API 不同而是 HTTP 协议分工不同很多初学者会困惑“为什么要有req.query、req.params、req.body三种方式不能统一成一个req.data吗”这个问题的答案不在 Express 框架本身而在 HTTP 协议的设计哲学里。你可以把一次 HTTP 请求想象成一封寄往服务器的快递包裹而 Express 就是那个负责拆包、分类、登记的仓库管理员。req.params是包裹外箱上贴的“收件人专属标签”比如/api/orders/12345这里的12345是路径的一部分它定义了资源的唯一身份就像快递单号本身。Express 在匹配路由时就已将这个数字从 URL 字符串中精准抠出来存进req.params.id。它不参与数据传输只负责定位资源。req.query则是包裹外箱上手写的“备注信息”比如/api/orders?statuspaidpage2这些键值对附加在 URL 末尾用?分隔用连接。它们是可选的、非必需的过滤条件或元信息浏览器地址栏里的一切?后面的内容都会被自动解析为req.query对象。而req.body才是包裹里面真正装的东西——可能是纸质订单application/x-www-form-urlencoded表单、一份电子合同application/json、甚至是一张产品照片multipart/form-data。它的存在与否、格式如何完全取决于请求的Content-Type头和客户端发送的数据体。Express 默认对body是“盲视”的因为它无法预知你要发来的是 JSON 还是二进制图片所以必须由开发者明确告诉它“请用 JSON 解析器处理application/json类型的请求体”这就是express.json()中间件的作用。理解这三层的物理位置和协议角色是避免后续所有混乱的基石。它们不是 Express 的“功能选项”而是 HTTP 协议天然划分的三个数据通道。2.2req.paramsRESTful 路由的“身份证识别器”req.params的核心价值在于它让 API 设计具备了语义清晰和资源导向的特性。假设我们要设计一个用户管理 API传统方式可能写成/api/getUser?id123这里id是query参数而 RESTful 方式则是/api/users/123123成为了路径的一部分即params。这两种写法在技术上都能实现但语义天差地别。前者get是动词强调操作后者users/123是名词强调资源。Express 通过其强大的路由匹配引擎来实现params的提取。当你定义app.get(/api/users/:id, handler)时Express 并不是简单地做字符串匹配而是将/api/users/:id编译成一个正则表达式例如/^\/api\/users\/([^\/]?)\/?$/i然后用这个正则去匹配实际的请求路径/api/users/123。匹配成功后捕获组([^\/]?)中的内容123就被赋值给了req.params.id。这个过程是纯路径匹配与请求体、查询参数完全无关。因此req.params的使用有严格的前提第一路径中必须有带冒号的占位符如:id、:slug第二请求的 URL 路径必须精确匹配该模式。一个常见的误区是试图在POST /api/users这样的静态路径里使用req.params.id这是不可能的因为路径里根本没有:id这个占位符。另一个高发错误是占位符命名不一致比如路由定义为:userId但在 handler 里却写req.params.id结果自然是undefined。我见过最离谱的一次是同事把:order_id写成了:order-id用了短横线而 Express 的路由解析器会把-当作普通字符导致整个占位符失效调试了整整一个下午才找到原因。所以req.params的稳定性完全建立在你对路由定义和 URL 实际结构的绝对一致性上。2.3req.queryGET 请求的“便签纸”与安全边界req.query是最“轻量级”的参数载体它直接暴露在 URL 地址栏中这意味着它天生具有两个关键属性可见性和长度限制。可见性既是优点也是缺点。优点在于调试极其方便——你不需要启动 Postman直接在浏览器里改地址栏的?page3sortdate就能看到效果非常适合开发阶段的快速验证。缺点则是所有敏感信息绝不能放在这里比如?tokenabc123或?password123456这等于把钥匙挂在门把手上。长度限制则来自浏览器和服务器的双重约束。虽然 HTTP 协议本身没有规定 URL 长度但主流浏览器Chrome、Firefox通常将最大 URL 长度限制在 2000 个字符左右而某些代理服务器或 Web 服务器如 Nginx默认也会设置large_client_header_buffers来限制请求头大小间接影响了query的可用长度。因此req.query只适合传递少量、非敏感、用于过滤、排序、分页的元数据。在我们的电商订单查询接口中/api/orders?statusshippedlimit20offset40是标准用法status是业务状态limit和offset是分页参数它们都是轻量、公开、无副作用的。值得注意的是req.query的值永远是字符串类型即使你传的是?page1req.query.page的值也是字符串1而不是数字1。如果你要用它做数值计算必须手动parseInt(req.query.page)或Number(req.query.page)。我曾经在一个分页逻辑里忘了这一步导致offset limit计算变成了字符串拼接4020结果是4020直接跳过了几百条记录线上报错才被发现。此外req.query支持数组语法比如/api/orders?statuspaidstatusshippedExpress 会自动将其解析为req.query.status [paid, shipped]这在多选过滤场景下非常实用但前提是你的前端构造 URL 时要遵循这个规则。2.4req.bodyPOST/PUT/PATCH 请求的“数据保险箱”但钥匙得你自己配如果说req.params和req.query是 Express 自带的“出厂设置”那么req.body就是需要你亲手安装并校准的“精密仪器”。它的核心难点在于HTTP 请求体Body可以是任意格式而 Express 作为一个通用框架无法也不应该为所有可能的格式提供内置解析器。因此它采用了中间件Middleware的插拔式架构。你需要根据客户端发送的数据类型主动加载对应的解析中间件。最常见的两种是express.json()专门解析Content-Type: application/json的请求体。它会读取整个请求流将其解析为 JavaScript 对象并挂载到req.body上。express.urlencoded({ extended: true })解析Content-Type: application/x-www-form-urlencoded的请求体也就是 HTML 表单提交的标准格式。extended: true这个选项至关重要它启用了qs库来解析嵌套对象比如表单里有user[name]Johnuser[email]johnexample.comreq.body就会是{ user: { name: John, email: johnexample.com } }。如果设为false则只能解析扁平结构嵌套字段会丢失。这两个中间件必须在定义任何路由之前通过app.use()全局注册。顺序也很重要express.json()和express.urlencoded()必须放在所有app.get()、app.post()等路由定义之前否则当请求到达路由 handler 时req.body还未被解析自然为空。这是一个新手百试不爽的坑。我曾帮一个团队排查一个“POST 接口始终收不到数据”的问题翻遍了前后端代码最后发现只是app.use(express.json())这一行被不小心写在了app.post(/login, ...)之后。请求先匹配到了/login路由handler 执行时req.body还是原始的 Buffer 流当然为空。把中间件移到最前面问题瞬间解决。这再次印证了一个原则在 Express 中中间件的注册顺序就是请求处理的流水线顺序一步错步步错。3. 实操全流程从零搭建一个健壮的订单查询与修改接口3.1 环境初始化与基础骨架搭建我们从一个干净的项目开始确保每一步都可控、可复现。首先创建项目目录并初始化package.jsonmkdir express-param-demo cd express-param-demo npm init -y接着安装 Express 核心依赖npm install express现在创建主入口文件server.js。这里的关键是我们必须在任何路由定义之前就完成所有必要的中间件注册。下面是一个经过生产环境验证的、最小但完备的初始化模板const express require(express); const app express(); const PORT process.env.PORT || 3000; // 【核心步骤1】注册 JSON 解析中间件 // 它会检查请求头的 Content-Type 是否为 application/json // 如果是则解析请求体为 JS 对象挂载到 req.body app.use(express.json({ // 限制请求体大小防止恶意大文件攻击 limit: 10mb, // 严格模式只接受标准 JSON拒绝 { foo: bar, } 这样的尾随逗号 strict: true })); // 【核心步骤2】注册 URL 编码解析中间件 // extended: true 启用 qs 库支持嵌套对象解析 // limit 同样限制大小type 指定只处理 application/x-www-form-urlencoded app.use(express.urlencoded({ extended: true, limit: 10mb, type: application/x-www-form-urlencoded })); // 【核心步骤3】一个简单的健康检查路由用于验证服务是否启动 app.get(/health, (req, res) { res.json({ status: OK, timestamp: new Date().toISOString() }); }); // 【核心步骤4】启动服务器 app.listen(PORT, () { console.log(✅ Express server is running on http://localhost:${PORT}); console.log( Try: curl http://localhost:${PORT}/health); });这段代码看似简单但包含了三个至关重要的实践要点。第一limit参数不是可有可无的装饰而是安全防线。如果没有它一个恶意客户端可以发送一个几百 MB 的 JSON 字符串耗尽服务器内存造成拒绝服务DoS攻击。第二strict: true是一个强烈推荐的选项它强制要求 JSON 格式必须符合 RFC 7159 标准能及早发现前端因疏忽产生的格式错误如多了一个逗号避免数据解析失败后难以定位。第三/health路由是微服务架构中的标准实践它不处理业务逻辑只返回一个简单的状态供 Kubernetes、Docker Swarm 等编排系统进行健康探针Liveness Probe检测。运行node server.js然后在另一个终端执行curl http://localhost:3000/health你应该能看到{status:OK,...}的响应。这证明你的 Express 服务骨架已经搭建完毕中间件也已正确加载。3.2 实现req.params基于路径的订单详情获取现在我们来实现第一个核心功能通过订单 ID 获取单个订单的详情。这正是req.params的经典应用场景。我们定义一个 GET 路由/api/orders/:id其中:id是一个动态参数。// 在 server.js 中/health 路由之后添加以下代码 // 【核心步骤5】定义基于 params 的订单详情路由 app.get(/api/orders/:id, (req, res) { // 1. 从 req.params 中安全地提取 id const orderId req.params.id; // 2. 【关键验证】检查 id 是否存在且非空 if (!orderId || orderId.trim() ) { return res.status(400).json({ error: Bad Request, message: Order ID is required in the URL path }); } // 3. 【关键验证】检查 id 是否为合法的数字假设我们的订单ID是数字 // 这里使用 parseInt 并检查 NaN比正则更直观 const parsedId parseInt(orderId, 10); if (isNaN(parsedId) || parsedId 0) { return res.status(400).json({ error: Bad Request, message: Order ID must be a positive integer }); } // 4. 【模拟数据库查询】在真实项目中这里会调用数据库 // 例如const order await db.orders.findById(parsedId); const mockOrder { id: parsedId, customerName: 张三, email: zhangsanexample.com, totalAmount: 299.99, status: shipped, createdAt: 2023-10-05T08:30:00Z }; // 5. 返回成功响应 res.status(200).json({ success: true, data: mockOrder }); });这段代码展示了req.params的完整实操流程。首先我们直接从req.params.id获取值这是 Express 已经为我们做好的工作。但紧接着我们进行了两层防御性编程第一层是空值检查防止undefined或空字符串进入后续逻辑第二层是类型和范围检查确保它是一个有效的正整数。这一步在生产环境中绝不能省略因为req.params的来源是 URL 路径而路径可以被任何人随意构造比如/api/orders/后面没跟任何东西或/api/orders/abc跟了非法字符。如果不加验证直接拿parsedId去查数据库轻则查询失败重则引发 SQL 注入如果后端拼接 SQL 字符串的话。最后我们返回了一个结构化的 JSON 响应包含success标志和data主体这是一种比裸露数据更专业的 API 设计习惯。你可以用curl来测试它# 正常请求 curl http://localhost:3000/api/orders/123 # 错误请求空ID curl http://localhost:3000/api/orders/ # 错误请求非法字符 curl http://localhost:3000/api/orders/abc你会看到前一个返回了订单数据后两个都返回了 400 错误和清晰的错误信息。这种“Fail Fast”快速失败的策略能让前端开发者在第一时间就知道问题出在哪里而不是收到一个模糊的 500 内部错误。3.3 实现req.query支持分页、筛选与排序的订单列表查询接下来我们实现一个更复杂的 GET 接口获取订单列表。这个接口需要支持多种查询条件这正是req.query的主场。我们将定义/api/orders这个路由并允许通过查询参数来控制返回的数据。// 【核心步骤6】定义基于 query 的订单列表路由 app.get(/api/orders, (req, res) { // 1. 从 req.query 中提取所有可能的参数 const { status, page 1, limit 10, sortBy createdAt, sortOrder desc } req.query; // 2. 【关键转换】将字符串参数转换为所需类型 const pageNum parseInt(page, 10); const limitNum parseInt(limit, 10); const offset (pageNum - 1) * limitNum; // 3. 【关键验证】对转换后的数值进行合理性检查 if (isNaN(pageNum) || pageNum 1) { return res.status(400).json({ error: Bad Request, message: Page number must be a positive integer }); } if (isNaN(limitNum) || limitNum 1 || limitNum 100) { return res.status(400).json({ error: Bad Request, message: Limit must be between 1 and 100 }); } // 4. 【关键验证】对枚举型参数进行白名单检查 const validStatuses [pending, paid, shipped, delivered, cancelled]; if (status !validStatuses.includes(status)) { return res.status(400).json({ error: Bad Request, message: Status must be one of: ${validStatuses.join(, )} }); } const validSortFields [id, totalAmount, createdAt, status]; if (!validSortFields.includes(sortBy)) { return res.status(400).json({ error: Bad Request, message: Sort by field must be one of: ${validSortFields.join(, )} }); } if (sortOrder ! asc sortOrder ! desc) { return res.status(400).json({ error: Bad Request, message: Sort order must be asc or desc }); } // 5. 【模拟数据库查询】构建查询条件对象 let queryConditions {}; if (status) { queryConditions.status status; } // 6. 【模拟分页与排序】在真实项目中这会转化为 SQL 的 WHERE, LIMIT, OFFSET, ORDER BY // 这里我们只返回一个模拟的响应结构 const mockOrders [ { id: 1001, customerName: 李四, totalAmount: 150.00, status: paid, createdAt: 2023-10-04T14:20:00Z }, { id: 1002, customerName: 王五, totalAmount: 89.99, status: shipped, createdAt: 2023-10-04T10:15:00Z } ]; res.status(200).json({ success: true, data: mockOrders, pagination: { page: pageNum, limit: limitNum, total: 150, // 模拟总记录数 totalPages: Math.ceil(150 / limitNum) } }); });这个路由比上一个复杂得多但它完美体现了req.query的威力和挑战。我们使用了 ES6 的解构赋值语法为page和limit提供了默认值1和10这使得/api/orders这个最简 URL 也能返回第一页的 10 条数据用户体验友好。紧接着我们进行了严格的类型转换和验证。page和limit必须是数字且limit还被限制在 1-100 的合理范围内防止用户恶意请求?limit1000000导致数据库压力过大。对于status这样的枚举参数我们定义了一个白名单数组validStatuses并用includes()方法进行检查。这是一种比正则表达式更易读、更易维护的验证方式。同样sortBy和sortOrder也做了白名单校验。这种“白名单优于黑名单”的安全原则是构建健壮 API 的基石。最后我们构建了一个queryConditions对象它将在真实项目中被传递给数据库 ORM如 Sequelize 或 TypeORM最终生成WHERE status ?这样的 SQL 语句。响应体中我们不仅返回了数据还附带了分页元信息pagination这让前端可以轻松地渲染分页控件。你可以这样测试# 获取第1页每页5条状态为 shipped curl http://localhost:3000/api/orders?statusshippedpage1limit5 # 获取按金额升序排列的第2页 curl http://localhost:3000/api/orders?sortBytotalAmountsortOrderascpage23.4 实现req.body接收 JSON 数据修改订单状态最后也是最关键的一步实现一个 POST 接口用于接收前端发来的 JSON 数据修改订单的状态。这将全面检验express.json()中间件是否工作正常。// 【核心步骤7】定义基于 body 的订单状态更新路由 app.post(/api/orders/:id/status, (req, res) { // 1. 从 req.params 提取订单ID复用之前的逻辑 const orderId req.params.id; if (!orderId || orderId.trim() ) { return res.status(400).json({ error: Bad Request, message: Order ID is required in the URL path }); } const parsedId parseInt(orderId, 10); if (isNaN(parsedId) || parsedId 0) { return res.status(400).json({ error: Bad Request, message: Order ID must be a positive integer }); } // 2. 【核心检查】确认 req.body 是否已被正确解析 // 如果这里打印出来是 undefined说明 express.json() 中间件没起作用 console.log(Raw req.body:, req.body); console.log(Type of req.body:, typeof req.body); // 3. 从 req.body 中提取新状态 const { status } req.body; // 4. 【关键验证】检查 status 是否存在且为字符串 if (!status || typeof status ! string) { return res.status(400).json({ error: Bad Request, message: Status is required and must be a string }); } // 5. 【关键验证】再次进行白名单检查 const validStatuses [pending, paid, shipped, delivered, cancelled]; if (!validStatuses.includes(status)) { return res.status(400).json({ error: Bad Request, message: Status must be one of: ${validStatuses.join(, )} }); } // 6. 【模拟数据库更新】在真实项目中这里会执行 UPDATE SQL // 例如await db.orders.update({ status }, { where: { id: parsedId } }); const updatedOrder { id: parsedId, status: status, updatedAt: new Date().toISOString() }; res.status(200).json({ success: true, message: Order status updated successfully, data: updatedOrder }); });这个 POST 路由是整个链条的“压力测试点”。它同时用到了req.params获取:id和req.body获取 JSON 数据是对前面所有配置的终极验证。代码中的console.log语句是调试的黄金法则。当你发现req.body是undefined时第一反应不应该是去改业务逻辑而是立刻看这个console.log的输出。如果req.body是undefined而typeof req.body是object那说明中间件工作了但数据本身有问题如果typeof req.body是undefined那 100% 是express.json()没有被正确注册或顺序错了。我们再次对status进行了白名单校验确保只有预定义的状态才能被设置这从源头上杜绝了非法状态的注入。注意这个路由的路径是/api/orders/:id/status它结合了paramsid和query没有以及bodystatus展示了三种参数在同一个接口中的协同工作。测试它需要一个真正的 JSON 请求体curl的-H和-d参数是最佳选择# 正确请求更新订单123的状态为 shipped curl -X POST \ -H Content-Type: application/json \ -d {status:shipped} \ http://localhost:3000/api/orders/123/status # 错误请求缺少 Content-Type 头此时 express.json() 不会触发 curl -X POST \ -d {status:shipped} \ http://localhost:3000/api/orders/123/status # 错误请求发送了错误的 JSON 格式多了逗号 curl -X POST \ -H Content-Type: application/json \ -d {status:shipped,} \ http://localhost:3000/api/orders/123/status第一个命令会成功后两个会失败并且失败的原因各不相同这正是我们通过日志和验证逻辑能够精准定位的。4. 常见问题与排查技巧实录那些年我们一起踩过的坑4.1 “req.body is undefined” —— 最高频、最让人抓狂的谜题这个问题的出现频率之高几乎可以申请吉尼斯纪录。根据我过去三年在 Stack Overflow 和公司内部知识库的统计超过 65% 的 Express 新手求助都与此相关。它不是一个单一原因造成的而是一个由多个环节组成的“故障链”。下面这张表格总结了最常见的五种原因、对应的排查方法和解决方案故障原因如何快速验证解决方案我的实操心得中间件未注册或顺序错误在server.js开头console.log(Before routes)在路由 handler 里console.log(In route)看哪个先打印。如果In route先打印说明中间件在路由之后。将app.use(express.json())和app.use(express.urlencoded(...))移到所有app.get()、app.post()之前。这是压倒性第一原因。我见过最离谱的案例是有人把中间件注册写在了app.listen()之后服务根本启动不了。记住口诀“中间件在前路由在后”。请求头 Content-Type 缺失或错误用浏览器开发者工具的 Network 标签页查看请求的 Headers确认Content-Type是否为application/json或application/x-www-form-urlencoded。在curl命令中显式添加-H Content-Type: application/json在 Axios 中确保headers: { Content-Type: application/json }。很多前端同学会忽略这个头。Axios 默认对POST发送 JSON 时会自动加但原生fetch不会必须手动headers: { Content-Type: application/json }。JSON 格式不合法将你发送的 JSON 字符串粘贴到 JSONLint 网站进行格式校验。修正 JSON 语法如去掉尾随逗号、确保引号是英文双引号、括号匹配。express.json({ strict: true })就是为了捕捉这类错误。开启它能让错误在框架层就暴露而不是等到业务逻辑里req.body.status报Cannot read property status of undefined。请求体过大超出中间件 limit查看 Node.js 控制台是否有PayloadTooLargeError错误。在express.json()和express.urlencoded()中增加limit选项如limit: 50mb。生产环境建议limit设置为10mb足够应付绝大多数业务。如果真有上传大文件的需求应该用专门的文件上传中间件如multer而不是塞进body。客户端发送了 multipart/form-data文件上传检查Content-Type头是否以multipart/form-data; boundary开头。express.json()和express.urlencoded()完全无法解析multipart。必须使用multer等专用中间件。这是另一个高频陷阱。HTML 表单如果包含input typefile浏览器会自动将Content-Type设为multipart。此时req.body会是空的而文件信息在req.files里。提示当你第一次遇到req.body is undefined时不要慌。拿出一张纸按照上面表格的顺序一项一项地检查。90% 的问题都能在 5 分钟内定位并解决。把这套排查流程变成肌肉记忆你的开发效率会提升一个数量级。4.2req.params和req.query的混淆与误用另一个常见误区是把本该用req.query的地方错误地塞进了req.params或者反之。这通常源于对 RESTful 设计原则的理解偏差。错误示范1把过滤条件放进 paramsGET /api/orders/status/paid这看起来很“酷”但它是反模式的。status是一个可变的、非唯一的过滤条件它不应该成为资源路径的一部分。正确的做法是GET /api/orders?statuspaid。因为/api/orders/status/paid这个路径从语义上暗示了“有一个叫paid的具体订单状态资源”而实际上paid只是一个状态值不是资源本身。错误示范2把唯一标识放进 queryGET /api/orders?id123这违背了 RESTful 的核心思想。123是订单的唯一身份它应该像身份证号一样是路径的固有组成部分而不是一个可选的备注。GET /api/orders/123才是正确的它清晰地表达了“我要获取 ID 为 123 的那个订单”。错误示范3在 POST 路由中错误地依赖 paramsPOST /api/orders/create然后在 handler 里试图用req.params.id。这是不可能的因为/create这个路径里根本没有:id占位符。POST的目的通常是创建新资源新资源的 ID 通常由后端生成并返回而不是由前端在 URL 路径里指定。我总结了一个简单的决策树帮你快速判断该用哪个这个值是用来唯一标识一个具体的、已存在的资源吗→ 是 → 用req.params如/users/123,/products/abc。这个值是用来对一个资源集合进行筛选、排序、分页的吗→ 是 → 用req.query如/users?roleadminpage2,/products?categoryelectronicssortprice。这个值是你要创建、更新或删除的核心数据内容吗→ 是 → 用req.body如POST /users的 body 包含{ name: John, email: johnexample.com }。注意一个请求可以同时使用三者但它们的角色绝不重叠。POST /api/orders/123/status中123是params定位订单status是body要更新的数据而?forcetrue这样的额外标志就可以是query表示强制更新覆盖某些业务规则