☰
RESTful接口设计:资源、路径与方法的完整落地指南
2026/10/2 8:41:53 网站建设 项目流程

说实话,很多项目里都在用 RESTful 这个词,但真正能把资源、路径、方法这三者的关系讲清楚、并且落到代码里的团队,其实不算多。日常评审接口的时候,经常能看到 /getUserInfo、/addOrder、/deleteUserById 这类路径,一看就知道设计者对 REST 的理解还停留在“URL 好看一点”的层面。这篇内容不聊虚的,直接把 RESTful 的资源、路径与方法对应关系拆开,从命名规则到完整设计案例,给出可以直接照抄的接口设计方案。后端写接口的、前端调接口的、以及正在给团队做接口规范的人,都值得花几分钟过一遍。

1. 资源:先想明白你在操作什么

1.1 资源的本质是抽象实体,不是数据库表

REST 里的“资源”指的是可以被识别、访问、操作的信息实体,比如一个用户、一笔订单、一篇文章、一张图片,甚至一台设备的状态。它的核心特征是:有唯一的标识,并且能够通过某种表现形式对外交互。很多人容易把资源和数据库表划等号,这是最常见的误解。

数据库表是存储层的概念,资源是业务层的抽象。同一个资源背后可能关联了三张表的数据,也可能只是缓存里的一段 JSON,但对调用方来说,它感知到的始终是“资源”本身。比如 GET /orders/123 返回的 JSON 里可能包含订单号、商品列表、收货地址、支付状态,这些数据在数据库里分布在 order、order_item、payment 好几张表,但对外它就是一个完整的订单资源。

想清楚资源还有一个实际好处:接口评审的时候,争论“字段应该放这里还是那里”之前,先确认“我们操作的是哪个资源”,很多分歧会自然消失。资源定不下来,后面路径和方法全都是在空中楼阁上做设计。

1.2 资源命名:为什么普遍要求名词复数、小写、中划线

先看结论,再解释原因。一套团队接口规范里最常见的资源命名要求是这样几条:

  • 一律使用名词,禁止动词
  • 集合资源用复数形式,比如 users、orders、articles
  • 路径统一小写字母
  • 单词间用中划线连接,比如 user-profiles,而不是 user_profiles
  • 不出现空格和中文,特殊字符全部做 URL 编码

为什么用名词不用动词?因为 REST 的设计思想是“把动作交给 HTTP 方法,路径只描述事物”。你用 GET /orders 表示“获取订单集合”,语义已经完整了,没必要再来一个 GET /getOrders。路径里出现 get、add、update、delete 这类动词,说明设计者还在用 RPC 的思维写接口,没有真正转向面向资源。

为什么集合用复数而单个资源不单独设一个单数路径?这是个经典争论。行业主流做法是:集合资源统一用复数,单条资源通过 id 去定位,即 /users/{id} 表示用户集合中的一个具体用户。这样路径的规律特别统一,不存在 /user/{id} 和 /users 之间那种“单复数混用”的别扭感。如果团队内部约定用单数也不是不行,但最怕的是同一个系统里 orders、order、users、user 并存,看路径的人还得猜。

为什么小写加中划线?URL 在部分服务器和代理上是区分大小写的,/Users 和 /users 可能被当成两个不同的资源,线上出过太多这种诡异问题。统一小写能从源头消灭这一类故障。中划线是行业惯例,视觉上比下划线更清爽,而且在部分浏览器和编辑器里,长 URL 换行时中划线更容易被正确识别。

1.3 集合与实例:路径里最容易混淆的两种身份

理解了资源,路径里就只剩两种身份要处理:集合资源和实例资源。

  • 集合资源:表示“一组同类资源”,比如 /orders
  • 实例资源:表示“集合里的某一个具体成员”,比如 /orders/{orderId}

这两种身份对应的操作有天然分工。集合资源上通常是查询列表和新增,对应 GET 和 POST;实例资源上通常是查询详情、更新和删除,对应 GET、PUT、PATCH、DELETE。合同一下:凡是路径以资源名词结尾的就是集合,凡是带 {id} 的就是实例,这个规律一确立,前后端联调时的沟通成本会低很多。

还有一层递进关系:子资源。比如 /users/{userId}/orders 表示“某个用户的订单集合”,它表达的是一种从属关系——订单归属于用户。再下一步是 /users/{userId}/orders/{orderId},表示“某个用户下的某个订单”,这种路径就是典型的层级关系设计。

但层级不是越深越好。我见过一些接口,一路嵌套到四五层,比如 /schools/{schoolId}/classes/{classId}/students/{studentId}/scores/{scoreId},写起来累,读起来更累。路径层级本质上是表达“包含关系”的,超过三层基本说明建模出了问题。大多数情况下,拆成扁平资源加查询参数是更优解,后面讲路径部分再细说。

2. 路径:把地址设计成一眼能看懂的结构

2.1 路径层级是从大到小的逐级收敛

路径是资源在 HTTP 世界里的门牌号。一个好的路径设计,应该让人从第一个单词开始就能猜到“你要操作什么范围的什么资源”。

路径从左到右,范围是逐步收窄的。拿 /api/v1/users/{userId}/orders/{orderId} 来举例:/api 是全局前缀,/v1 是版本范围,/users 是用户集合,/{userId} 是具体用户,/orders 是订单子集合,/{orderId} 是具体订单。每一级都在缩小范围,最后定位到唯一目标。这和我们寄快递填地址的逻辑一模一样:省、市、区、街道、门牌号,一级一级往下收敛。

记住这个“逐级收敛”的原则,设计路径时就会少很多纠结。考虑“订单详情接口该怎么写”的时候,先问自己:订单是否归属于某个上级资源?如果需要按用户维度去查,那 /users/{userId}/orders/{orderId} 是合理的;如果订单本身就是一个全局概念,后端直接按订单号查所有订单,那 /orders/{orderId} 就够了,没必要硬套一层用户进去。

2.2 路径参数与查询参数,各有各的边界

路径参数和查询参数的边界,是接口设计里非常常见的一个分水岭。

路径参数的作用是定位资源身份。/users/42 里的 42 是用户资源的 ID,它直接决定了你访问的是哪个资源。换句话说,少了这个参数,资源就不存在了。查询参数的作用是对资源集合做过滤、排序、分页、投影。比如 GET /users?role=admin&page=1&size=20,role 和 page 不是资源身份,而是针对集合的筛选条件。

很多人分不清这二者,设计出了 /users/{id}/orders/paid 这种路径,想表达“这个用户下已支付的订单”。这里 paid 是一种状态筛选条件,不是订单资源集合中的一个实例,把它塞进路径里属于语义错位。规范化做法是 /users/{id}/orders?status=paid。如果后面还要增加“已取消”“已退款”的筛选,路径方案就得不断加层级,查询参数方案只需要多传一个参数。

query string 设计的另外一个原则是:偏“条件”的都放查询参数,偏“身份”的都放路径参数。例如分页用的 page 和 size、排序用的 sort、筛选用的 status、搜索用的 keyword,全部都是查询参数;而资源的 id、子资源 id,全部是路径参数。边界一旦定下来,接口表格写起来思路也会清晰很多。

2.3 版本号、分隔符和其他容易忽略的路径细节

版本号建议直接放在路径最前面,即 /api/v1/orders 这种形式。相比用请求头传版本号的方式,路径版本号最直观,curl 调试方便、网关路由配置也简单、前端出错时日志里也能一眼看出来调用的是哪个版本。虽然语义上请求头的做法更“干净”,但实际工程里路径版本号压倒性胜出,因为它把“版本”变成了接口地址的一部分,强制可见。

路径里还有几个细节值得统一:

  • 不要用 .json、.do、.action 这类扩展名作为路径结尾,它们是老式 MVC 框架留下的味道,对资源化设计没有意义。
  • 路径中的 ID 类型建议对外统一,不要一个接口用自增数字、另一个接口用 UUID,会让调用方非常难受。如果担心中间业务量变化,前期直接用不透明 ID 也是一种合理选择。
  • 分页参数建议统一成 page + size,比如 page=1&size=20,相比 offset/limit 更容易理解。重点不是用哪套,而是全团队只允许用一套。

路径本身还算不上最复杂的部分,真正的难点在于把 HTTP 方法和“对资源的操作意图”对应起来,这是下一部分要解决的问题。

3. 方法:用标准动作表达操作意图

3.1 五个核心 HTTP 方法的语义和幂等性

HTTP 协议给了一组现成的“标准动作”,RESTful 设计里最常用的就是五个:GET、POST、PUT、PATCH、DELETE。每一个动作有它精确的语义,乱用是接口混乱的重要来源。

GET 是读取操作,不改变资源状态,安全且幂等。它的特点是可以在浏览器地址栏直接访问、可以缓存、可以被预取,所以永远不要用 GET 去做删除或修改的操作。POST 是在集合资源下创建新资源,不是幂等的——你发两次 POST /orders,结果是创建两笔订单。PUT 是整体替换,语义上要求请求体包含资源的全部字段,结果就是“把旧的整个换掉”,因此从结果上讲,连续执行多次 PUT 效果一致,可视为幂等。PATCH 做部分更新,只提交需要修改的字段,它不保证幂等,因为同一个 PATCH 请求重放两次,中间状态可能不同。DELETE 是删除资源,幂等性比较好理解,删除一个已删除的资源,服务器直接返回 404 即可,不会再删一次。

生活化的理解方式:GET 是看橱窗,POST 是往篮子里加一个新商品,PUT 是把整个购物车换成另一套商品,PATCH 是改了购物车里的某件商品数量,DELETE 是把商品从购物车移除。动作本身不复杂,复杂的是工程世界里经常有人走捷径,比如用 POST 干所有事、把所有删除操作都写成 POST /deleteXXX,这种系统不是不能用,但时间长了接口会越来越难维护。

3.2 方法、资源、路径的完整对应关系表

把方法、资源形态、路径格式和成功状态码放到一张表里,就看得很清楚了:

方法资源形态路径格式操作用途成功状态码
GET集合/orders获取订单列表200 OK
GET实例/orders/{orderId}获取订单详情200 OK
POST集合/orders创建订单201 Created
PUT实例/orders/{orderId}整体替换订单200 OK
PATCH实例/orders/{orderId}部分更新订单字段200 OK
DELETE实例/orders/{orderId}删除订单204 No Content

这张表是 RESTful 路径与方法对应关系最核心的骨架。可以看到一个规律:集合资源主要承载两类操作,查询列表用 GET,新增用 POST;实例资源承载四类操作,查询详情用 GET,整体替换用 PUT,部分更新用 PATCH,删除用 DELETE。路径里的资源名词不变,变化的是前面的方法和后面的 id。

还有一个特殊形态:动作接口。当业务操作无法简单对应到 CRUD 时,比如“取消订单”“支付订单”“确认收货”,业界普遍采用 POST + 动作子路径的写法,例如 POST /orders/{orderId}/cancel、POST /orders/{orderId}/pay。这种写法在严格 REST 语义里算是一种妥协,但工程实践中非常实用,因为动作本身会触发复杂的副作用(库存、支付、通知等),硬压到 PATCH 里反而会把接口语义弄模糊。

3.3 状态码:补全结果语义,减少沟通成本

路径和方法解决了“做什么”的问题,状态码解决的是“结果如何”。很多团队的接口习惯是无论什么情况都返回 200,然后在业务码里写 1 表示失败、0 表示成功,这其实浪费了 HTTP 语义。

推荐这套常规对应关系:

  • 200 OK:GET、PUT、PATCH 操作成功
  • 201 Created:POST 创建成功,建议同时返回 Location 响应头,指向新资源的 URI
  • 204 No Content:DELETE 成功,无返回体
  • 400 Bad Request:请求参数格式不对或缺少必填字段
  • 401 Unauthorized:未认证或认证失效
  • 403 Forbidden:已认证但无权限操作
  • 404 Not Found:资源不存在,或路径资源层级不匹配
  • 409 Conflict:资源当前状态与操作冲突,比如删除一个已在付款中的订单
  • 422 Unprocessable Entity:请求格式正确但业务校验失败
  • 500 Internal Server Error:服务器内部异常

状态码用得好,前端联调时基本不用等后端解释就知道了问题出在哪个环节。比如收到 422 就直接查请求体的字段校验,收到 409 就知道业务状态流转不合法。这比所有错误都塞进一个 200 里,再靠返工解析 message 的方式要顺畅得多。

4. 实战拆解:一套订单接口的完整设计过程

4.1 从需求到资源建模

光讲概念容易飘,用一个实际需求把前面所有内容串起来。假设要给一套电商系统设计订单模块的接口,需求列出来大概是这样的:

  • 用户能查看自己的订单列表,支持按状态筛选、分页
  • 用户能查看某订单的详情
  • 用户能创建订单(带商品清单、收货地址)
  • 用户能修改订单的收货地址
  • 用户能取消订单
  • 前端需要展示订单里的商品明细

第一步不是写接口清单,而是做资源建模。很明显这里有订单的概念,所以核心资源就是 orders。订单里的商品明细 item 是依附于订单的子资源,可以建模成 orders 下的子资源集合。收货地址是订单的一个属性字段,不需要单独拆一个 address 资源。用户信息可以引用已有的 users 资源,不需要在订单模块里重复建模。

于是得到资源模型:orders 是顶层集合资源,orders/{orderId}/items 是订单行项子的资源路径前缀。这个模型足够覆盖全部需求,又不至于拆得太碎。

4.2 逐条把接口清单落成路径与方法

照着资源模型和需求清单,接口设计如下:

  • 查询订单列表:GET /api/v1/orders?status=paid&page=1&size=20&sort=createdAt,desc
  • 查询订单详情:GET /api/v1/orders/{orderId}
  • 创建订单:POST /api/v1/orders
  • 修改收货地址:PATCH /api/v1/orders/{orderId},请求体只带 address 字段
  • 取消订单:POST /api/v1/orders/{orderId}/cancel
  • 查询订单商品明细:GET /api/v1/orders/{orderId}/items

这套路径既遵守了集合和实例的规律,也把筛选条件正确地放进了查询参数,没有出现任何动词路径。查询订单列表的 GET 使用了 status 做筛选、page 和 size 做分页、sort 做排序,全部是查询参数。

创建订单为什么用 POST 而不是 PUT?因为客户端无法决定新订单的 ID,服务器收到请求后分配订单号,这是标准的“在集合下新增成员”场景,应该用 POST。创建成功后返回 201,并在响应头 Location 里给出新订单的访问地址。

修改收货地址为什么用 PATCH 而不是 PUT?因为只改一个字段,用 PUT 就得把订单所有字段都传一遍,订单这种资源字段量很大,整体替换不仅浪费带宽,还容易覆盖并发环境下的其他修改。PATCH 的语义就是局部更新,请求体里只需要 address 就是最优解。

4.3 动作接口的取舍:为什么取消订单不走 DELETE

这是接口设计里最需要说明的一笔。从直觉上看,“取消订单”和“删除订单”很像,很容易被写成 DELETE /orders/{orderId}。但在真实业务里,订单取消不是物理删除,而是一次状态流转:订单从 paid 变成 cancelled,同时要回补库存、走支付原路退回的逻辑,如果订单已经处于 shipping 状态,取消还可能被拒绝。

把这样一个带复杂副作用的操作直接映射成 DELETE,会有两个问题:一是 DELETE 在语义上表示“移除资源”,但订单数据必须留存,不能删;二是状态流转存在业务规则,比如已发货订单不能取消,这需要专门的动作语义来表达。因此这里选择 POST /orders/{orderId}/cancel,表示“触发取消动作”。这是一种对 REST 的务实扩展,业界普遍认可。

如果团队偏好更严格的 RESTful 风格,也可以用 PATCH /orders/{orderId} 加请求体 {"status": "cancelled"},把取消操作表达为“修改状态字段”。两种方式都可以,关键是全团队约定一致,不要一个项目里两种风格并存。我个人在做大型业务系统时会偏向显式动作接口,因为动作逻辑复杂,单独成端点更好维护,文档也好描写。

4.4 返回结构与错误信息也要统一

路径和方法定好后再补两个规范:返回结构和错误格式。

订单列表接口的返回结构建议用分页信封包一层:{ "page": 1, "size": 20, "total": 103, "items": [...] },这样前端拿总条数渲染分页组件时不需要额外请求。订单详情和创建接口直接返回订单对象本身就行。如果团队习惯统一包一层 code/data/message 也不是不行,但不要一套系统里有的接口包一层、有的裸返回,前后端联调时最怕这种不一致。

错误响应至少要有三个字段:code、message、details。code 表示错误码,message 是人可读的概要,details 是具体字段级错误。比如创建订单时商品库存不足,返回 409,body 是 {"code": "ORDER_OUT_OF_STOCK", "message": "商品库存不足", "details": {"productId": "1001", "available": 3}}。这种结构前端做错误提示时非常省力。

5. 常见问题与排查技巧实录

5.1 设计阶段的典型错误:动词路径、单复数混用、层级过深

接口设计阶段的错误,发现越早修复成本越低。整理几个高频问题:

动词路径是出现频次最高的问题。/getUserInfo、/addOrder、/deleteOrderById 这类写法在项目里非常顽固,根本原因是很多开发者习惯了 RPC 风格的接口命名。改法其实很机械:把动词删掉,换成对应的 HTTP 方法,路径只留资源名词。GET /users/{id} 就是原来的 getUserInfo,POST /orders 就是原来的 addOrder。

单复数混用排第二。/users 和 /user 同时出现,orders 和 order 并存,会让调用方很痛苦。还有人会误把“单个资源的复数形式”当成路径规则,比如 GET /user/{id},这其实是单数实例路径。解决方案是团队规范里写明“集合一律复数,实例用 {id} 定位”,代码评审时专门盯这一类问题。

层级过深和资源拆分不当是更隐蔽的问题。有人把接口按页面 UI 结构来设计,页面是“学校-班级-学生-成绩”,接口路径就跟着写四层。结果成绩接口被另一个页面单独使用,还要硬传班级 id 和学校 id 去拼路径。这类问题的根源是没做独立的资源建模。正确做法是:能定位资源的 id 一律扁平化,比如成绩资源就放到 /scores/{scoreId},查询条件用 query string 传 schoolId 和 classId。

5.2 联调阶段的方法与路径不一致

联调阶段的问题往往不是设计错了,而是实现端和调用端对同一份接口文档的理解不一致。举几个真实场景:

GET 请求带 body。前端用 axios 调 GET 时习惯性地把筛选参数放进了 data 字段,后端框架在不解析 body 的情况下收到空参数,结果接口返回了全量数据。排查了半天,最后发现是请求参数位置放错了。规则很简单:GET 的参数一律拼在 URL 查询串里,后端也只从 query string 读取。这个约定要写进文档。

DELETE 请求期望返回体。后端用标准 REST 语义返回 204 No Content,前端却用 response.data 去解析返回结果,解析到空后报错。解决方法是前端识别 204 状态时不读 body,或者后端约定 DELETE 也返回 200 加一个简单响应体。两种方案都行,但别在一个项目里一半接口返回 204 一半返回 200。

405 Method Not Allowed 是最容易排查也最容易犯的低级错误。前端调 PATCH /orders/{id},后端路由却只注册了 PUT 的 handler,于是框架直接返回 405。解决办法是检查路由表,把实际注册的 HTTP 方法对出来。真正头疼的情况是反向代理层把某些方法拦截了,或者 CDN 缓存把 POST 请求当成了 GET 处理,这不属于应用代码问题,需要看代理转发日志。

5.3 接口自查清单与避坑经验

下面这张清单,我每次设计完接口都会过一遍,建议你也打印出来放在手边:

检查项正确形态错误示例
资源是否用名词orders、usersgetOrders、addUser
集合是否用复数/orders/order
路径是否统一小写/users/{id}/Users/{id}
筛选是否放查询参数?status=paid/orders/paid
实例是否用 {id}/orders/{orderId}/orders/getById
创建是否用 POSTPOST /ordersPOST /orders/add
局部更新是否用 PATCHPATCH /orders/{id}PUT 全量覆盖
动作是否有独立端点POST /orders/{id}/cancelDELETE 业务数据
成功状态码是否语义正确201 创建成功统一 200
错误是否带业务码code+message+details只有 message

避坑经验忍不住多说一条:幂等性设计时永远把“重试”考虑进去。前端如果没收到响应,通常会做超时重试。假如重试的是 POST 创建接口,就可能重复创建两笔订单。解决方案是在创建接口里加入幂等键机制——客户端每次创建一个新订单时传一个全局唯一的请求 ID,服务端记录这个 ID,重复 POST 直接返回第一次创建的订单结果。虽然这不是 REST 规范强制要求的,但工程上几乎没有例外都需要它。

最后分享一个我自己坚持了很久的习惯:设计任何一组接口时,先拿一张纸把所有资源名词列出来,再给每个资源配上集合和实例两种路径形态,最后才把方法逐个填进去。这个顺序走顺了,动词路径基本不会出现。另一个实用技巧是设计完用一句话朗读同理,“对订单集合发起 POST 表示创建订单,对订单实例发起 PATCH 表示修改部分字段”,如果这句话读起来别扭,大概率是语义有偏差,趁早调整再进入开发,能帮团队省下大量联调返工的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询