资讯动态

什么是RESTful API设计?从资源、HTTP方法到幂等性的完整指南

发布时间:2026/10/13 11:54:24 来源:尧图企业网站定制
前两年团队做接口联调时我发现一个特别普遍的现象不少后端同学把“RESTful”理解成了“URL长得好看”。能精准区分GET和POST却搞不清PUT和PATCH到底该用哪个返回码一律200报错也靠code字段硬扛更别谈什么无状态、可缓存这些约束。标题这句话“什么是RESTful风格的API设计”看起来基础但真要系统讲透反而比一些框架源码更值得聊。这篇文章我就从概念起源、六大约束、资源设计、方法语义、实操流程到踩坑排查把RESTful设计完整梳理一遍。适合需要写接口的开发者、刚接触前后端协作的新人以及想优化存量接口设计的人。1. 内容整体设计与思路拆解1.1 REST到底解决的是什么问题回到2000年Fielding博士在论文里提出RESTRepresentational State Transfer表现层状态转移时针对的其实是传统RPC风格接口的痛点。早期很多接口是这么设计的/getUserInfo、/createOrder、/deleteGoods动词塞进URL一个资源一套接口随着业务膨胀接口数量爆炸协作成本极高。REST的核心思路是反过来的——资源是名词HTTP方法才是动词URL只负责定位“资源”GET/POST/PUT/DELETE负责表达“对这个资源做什么操作”。这个转变带来一个直接收益接口从“业务的动词清单”变成了“资源的统一操作面”。比如用户这个资源不管你是查询、新增、改昵称还是注销URL都围绕/users展开方法区分动作。后端只需要维护一套路由体系前端也能靠“名词方法”的直觉猜出大部分接口的含义联调效率自然不一样。1.2 为什么很多团队学完还是一堆坑我观察下来不少团队不是不知道REST概念而是把“风格”当成了“规范全集”忽略了REST核心的三个词资源、表述、状态转移。资源好理解表述是指客户端和服务器传递的其实是资源在某时刻的“快照”比如JSON数据状态转移是指客户端通过HTTP方法让服务器端的资源状态发生变化。这三个词串起来才是完整的REST视角。同时REST强调“超媒体即应用状态引擎”HATEOAS意思是服务器返回的资源表述里应该带上下一步可执行的动作链接让客户端跟着链接走而不是硬编码URL。实践中大多数业务系统都没做到HATEOAS因为代价高、收益不明显。所以在落地时我们要分清“理想REST”和“务实REST”完全照搬论文会让团队痛苦完全抛开又会退回RPC老路。权衡之后多数团队的可行做法是严格用名词化URL和HTTP方法语义状态码用到位无状态和可缓存尽量贴近HATEOAS按需裁掉。这是全文讨论的基线。2. 核心概念前置资源、表述、无状态2.1 资源与表述的关系先聊资源和表述。一个资源不是数据库里那张表而是“可以被命名并暴露给客户端的一个业务实体”。以订单为例订单是资源/orders/10086定位到它但返回给客户端的是它在某一时刻的JSON表述。同一个资源可以有多种表述内部用XML、外部用JSON、管理端用CSV下载资源没变变的是表述形式。客户端通过Accept请求头声明想要哪种表述服务器通过Content-Type告诉客户端返回的是什么格式。这一点新手最容易忽略。不少团队把“返回结构”当成一件约定俗成的事接口文档里直接写死“返回字段如下”。但REST的思路是资源模型稳定表述层灵活。哪天对接方要求多一个字段、少一个层级正确做法是调整表述适配层而不是把资源模型改了。我在一些项目里见过接口文档里出现“下单传参order_status_code”这就是资源模型和表述混为一谈的典型问题。2.2 无状态约束的真正含义无状态Stateless是REST里最容易被误解的词。它不是说服务端不能存任何状态而是说请求的上下文必须由客户端完整携带服务端不靠会话上下文来理解请求。用户点击“查看购物车”请求里要带着用户身份的令牌连续操作的中间状态也保存在客户端或独立的缓存服务里不能依赖服务端Session。这个约束的直接价值是水平扩展容易。服务端没有会话黏性前面挂N个实例负载均衡随便轮询哪个实例处理请求都不影响正确性。对运维来说这是实打实的红利。落地时注意用户登录态怎么办主流做法是Token机制JWT这类自包含令牌尤其适合服务端验签即可不需要存会话。2.3 为什么统一接口是REST的命根子REST的四个约束资源标识、资源表述、自描述消息、超媒体驱动合起来叫“统一接口”。翻译成大白话就是所有资源都用同一套路数来操作客户端对A资源和B资源的理解方式完全一致。自描述消息也值得单独说——请求里应该带够足够的元信息方法、Content-Type、Accept、认证头让中间层和服务端不需要额外“查文档”就能处理消息。现实里最常见的破坏统一接口的做法就是“动词URL”。比如/users/login这种接口一眼看过去就知道团队没有理解REST的意图。登录其实可以建模成“会话资源”的创建POST/sessions。但老实说很多团队觉得这样绕直接用/auth/login也无可厚非。我的建议是纯业务资源严格REST认证这类基础设施接口保持简洁约定。这里没有绝对对错关键是团队内部统一标准。3. 资源设计与URL命名规范3.1 URL只放名词且用复数资源定位是REST设计中最先落地也最能体现功力的部分。一条核心原则URL用名词不用动词用复数不用单数。对比一下/getUserInfo→/users/123/createOrder→ POST/orders/deleteGoods?id5→ DELETE/goods/5复数形式的好处是集合和单数的关系清晰。/users是用户集合/users/123是集合里的单个用户。新增用户就是向集合POST一个表述删用户就是对单个资源发DELETE。整个资源模型很自然。名词的选择也有讲究业界基本默认用英文名词不出现拼音和缩写混用。3.2 子资源的层级表达现实业务必然有嵌套资源。一个用户有多张银行卡表示层级关系时用/users/123/cards语义是“属于用户123的银行卡集合”。这里有个值得思考的问题层级要不要无限制嵌套我见过的经验是两层就好最多三层。因为层级越深接口越难维护前端拼URL也容易出错。而且层级的本质是“归属关系”但很多业务关系并不是严格的父子关系。比如“某个订单里的商品”商品本身是独立资源如果纠结于从订单找到商品可以用查询参数代替层级/orders/10086/items可以但如果商品要被多个订单引用更合理的做法是/order-items?order_id10086。所以设计子资源前先问一句这个子资源离开父资源还有独立生命周期吗如果有就不要嵌套用查询参数关联。3.3 查询参数做过滤、排序、分页集合型的GET接口几乎都要支持过滤、排序、分页标准做法是全部放查询参数。过滤GET /users?roleadmin排序GET /users?sort-created_at负号代表倒序分页GET /users?page2size20或GET /users?offset20limit20两种分页风格各有拥趸。基于页码的page/size适合后台管理类系统人肉翻页方便基于游标的cursor适合信息流因为新数据插入不会导致翻页错乱。要注意的是分页响应不要只返回数据数组最好把总数、页码、每页数量一并返回或者返回下一页的游标。前端渲染分页组件时需要知道总数否则只能再额外调一次COUNT接口浪费性能。3.4 一个完整资源URL设计示例以电商系统的商品和订单为例设计一组合理的URL功能设计示例说明商品列表GET/products?categoryphonesort-pricepage1size20过滤排序分页组合商品详情GET/products/52定位单个资源创建商品POST/products请求体是商品JSON表述更新商品PUT/products/52全量更新部分更新PATCH/products/52只传需要改的字段删除商品DELETE/products/52标记删除或物理删除均可订单列表GET/orders?statuspaidcursorxxx游标分页订单详情GET/orders/10086单个资源订单条目GET/orders/10086/items子资源集合4. HTTP方法与状态码语义4.1 五种核心方法的分工与语义HTTP方法在REST里就是“操作原语”每种方法的语义都是公共约定的理解透了你设计接口会顺很多。GET读取资源表述必须是安全的不改变服务端状态和幂等的重复调用结果一致。实际开发中见过有人用GET接口带JSON body做复杂查询虽然HTTP协议没禁止但绝大多数代理、缓存、日志组件不会处理GET body强烈不建议。POST在集合上创建新资源或触发一个“难以用PUT/PATCH表达的操作”。语义上是非幂等的——提交两次订单就是两张订单。PUT全量更新指定资源用请求体里的完整表述替换服务端资源。幂等。PATCH部分更新指定资源只需要传要改的字段。PATCH的幂等性需要看实现严格语义不保证幂等但通常业务上按幂等设计更安全。DELETE删除指定资源幂等。有个实操中经常被问到的问题POST和PUT到底啥区别最简单的判断标准客户端知道目标资源的URL吗知道就PUT对已有资源整体替换不知道就POST交给服务端生成新资源。比如创建订单时URL是/orders服务端生成订单号此时POST订单详情更新时URL是/orders/10086客户端明确知道要动哪个资源此时PUT或PATCH。4.2 状态码别只用200和500状态码是REST设计里信息量最密集的部分但实际联调时我看到最多的就是成功200、失败500其他一概不用。这就把HTTP状态码最重要的语义空间浪费了。常用状态码速查表状态码含义典型场景200 OK请求成功GET/PUT/PATCH成功201 Created资源创建成功POST创建新资源204 No Content成功但无返回体DELETE成功、PUT全量更新成功400 Bad Request请求体格式错误或参数缺失就客户端说“你传错了”401 Unauthorized未认证没带Token或Token失效403 Forbidden已认证但无权限普通用户访问管理员接口404 Not Found资源不存在URL拼错或数据被删409 Conflict资源状态冲突重复创建、版本冲突422 Unprocessable Entity请求语义错误校验失败比如手机号格式错429 Too Many Requests触发限流刷接口太频繁500 Internal Server Error服务端内部错误代码异常、数据库超时特别注意400和422的区别。400偏重“HTTP层面没法处理”比如json解析失败422偏重“业务校验没过”比如用户名已被占用。两者都返回时响应体里应带错误码、错误信息、可读的提示文案。200和201的区别也常被忽略创建资源时返回201并带上新资源URLLocation响应头这是REST的推荐做法但很多团队一律返回200前端也习惯了。建议从新建接口开始改习惯老接口逐步迁移。4.3 幂等性REST安全性的基石幂等性这个概念值得展开讲。一个操作幂等意思是执行一次和执行N次的效果完全一样。GET天然幂等PUT天然幂等DELETE按理说幂等删不存在的资源可以返回404状态没变。为什么看重幂等分布式环境下客户端可能因为超时重试、消息队列重复投递而重复调用接口。如果下单接口不幂等一次重试就多一个订单有了幂等设计重复请求只返回第一次的结果。落地幂等有几个常见做法幂等键——客户端在请求头里带一个唯一标识比如Idempotency-Key服务端用这个键做去重第一次处理完存下结果同一个键再来直接返回缓存结果。唯一约束——数据库给业务唯一键订单号、交易流水号加唯一索引重复插入直接被数据库拦下来。状态机校验——比如订单状态必须是“待支付”才能执行“支付”动作状态流转后重复请求会因状态不匹配而失败。5. 从零设计一套RESTful API的实操流程5.1 第一步梳理业务资源清单动手写路由前先别急着建Controller。把业务拆成“名词清单”每个名词就是一个候选资源。比如一个博客系统名词有用户、文章、评论、标签、分类、点赞记录。把这些名词列出来后再按生命周期判断哪些是独立资源、哪些是子资源。文章的评论可以建模成子资源/articles/1/comments因为评论离开文章没有独立意义标签就得独立成/tags因为标签可以被多篇文章共用。这一步想清楚后面所有URL设计都顺了。我见过团队跳过建模直接写路由最后接口URL五花八门前端文档翻到吐。5.2 第二步为每个资源确定方法与状态码资源清单出来后给每个资源配一张“方法-行为-状态码”表。以订单为例方法行为成功状态码失败状态码GET /orders查询订单列表200400参数错误POST /orders创建订单201422库存不足GET /orders/10086查单详情200404订单不存在PATCH /orders/10086修改收货地址200422地址不规范PUT /orders/10086全量更新用于对账修正200409订单状态锁定DELETE /orders/10086删除/作废订单204409订单已支付不可删配表的好处是接口的行为边界一眼看清也方便后期评审时逐项讨论。5.3 第三步过滤、分页、排序和字段选择集合接口几乎是高频流量入口参数设计必须提前约定。通用的建议是过滤参数名和资源字段保持一致避免同一个字段叫法不一排序用sortfield1,-field2格式支持多个排序条件分页默认值要定page从1开始size建议默认20、最大100防止拉全量字段裁剪用fieldsid,name,avatar减少大对象响应体对移动端尤其友好这步容易被当成“小事”忽略但实际上前后端联调中大多数争议都出在这。早定规范省掉大量口水。5.4 第四步设计统一响应结构REST响应到底要不要包一层data/code/message我见过两种极端。一种是完全裸返回资源数组直接给HTTP状态码就是唯一语义。另一种是包一层大壳code0代表成功HTTP状态码永远200。前者更贴近REST原教旨但异常信息不好统一表达后者方便前端统一处理但把HTTP状态码废了。我的折中方案成功响应裸返回资源表述失败响应统一错误结构。比如{ error: { code: ORDER_STATUS_CONFLICT, message: 订单已支付无法删除, details: {} } }这样成功路径简洁失败路径有统一结构日志和前端报错提示都容易做。是否要包data壳可以看团队习惯但我建议至少别把HTTP状态码全部废弃否则REST语义优势就没了。5.5 第五步定义版本策略接口一定会演进版本策略要在第一版就定好。三种主流方式方式示例优缺点URL路径版本/v1/users、/v2/users最直观适合外部开放API但版本多了URL丑请求头版本Accept: application/vnd.xxx.v2json干净但排查问题不直观查询参数版本/users?version2实现最简单但容易被人忽略对外部不可控的客户端我建议URL路径版本因为最不容易配错也最方便做兼容内部系统用查询参数或请求头都可以团队约定统一就行。6. 工具链与周边配套文档和调试6.1 用OpenAPI描述你的RESTful APIRESTful API设计得再好没有一份可靠文档协作体验也会崩塌。业界事实标准是OpenAPI Specification原Swagger。它用一份YAML或JSON描述所有路径、方法、参数、请求响应结构然后生成文档站点、客户端SDK和Mock服务。定义OpenAPI文件的过程本身也是检查资源设计的好机会写paths时你会发现URL规不规范、参数全不全、响应结构清不清晰。实践中建议用“代码注释生成自动校验”的方案Spring Boot配合springdoc、FastAPI配合自动生成Go的gin-swagger差不多一个套路。关键是CI里加一步OpenAPI规范校验格式错了直接挂流水线。6.2 调试工具怎么选接口调试日常主要用这几类curl命令行快速验接口适合脚本和排查问题Postman功能全集合管理、环境变量、自动化测试都方便适合联调和团队分享ApifoxPostman和Swagger的本地化合体中文团队用得不少浏览器开发者工具看请求头、Cookie、状态码最直接个人习惯是日常联调用Apifox遇到诡异问题开浏览器DevTools看完整请求链路排查信息更全。日志侧建议接口层统一打点记录method、path、status、耗时、error_code这些数据对排查线上问题价值极高。7. 常见问题与排查技巧实录7.1 到底要不要把动词放进URL遇到过最经典的灵魂拷问“导出报表不是动词吗它也得有接口啊。”确实有一部分操作没法单纯靠“增删改查”表达比如“导出”、“发送短信”、“触发邮件”。我的处理思路是先问能不能映射到资源状态变化上。导出报表→GET/reports/xxx/export把export当动作参数发送短信→POST/sms_messages创建一条发信任务本身就是资源。如果实在绕不开通用动作比如“批量审核”用自定义方法也不是十恶不赦但要规范命名POST/orders/batch-review并明确这是团队的便利约定不是REST标准。核心是默认贴近REST必要时保留例外但例外要有明确清单。7.2 嵌套资源到底该展平还是该嵌套有人习惯把所有关联都做成嵌套/users/123/orders/456/items/789诚然层级表达了归属但这种URL又长又脆任何一个中间层级字段变了URL就失效。排查时会很痛苦——到底是用户不存在还是订单不存在还是商品不存在我的建议是非要表达归属时按“业务聚合根”来定一个聚合根下最多两层子资源跨聚合的关联一律展平用查询参数连接。比如查用户下的订单用/orders?user_id123而不是/users/123/orders这样订单就是独立资源可以单独被其他系统引用。7.3 部分更新到底用PUT还是PATCH每次评审接口必争问题。我的建议分三种整个对象替换用PUT且要求客户端传完整资源只改一两个字段用PATCH不确定且团队无历史包袱直接用PATCH。但有一个细节容易被坑PATCH在部分框架里默认是全量更新的别名比如某个知名框架的PatchMapping实际执行的就是把空字段置空操作前一定要确认框架行为。7.4 文件上传算不算RESTful上传文件的接口经常被问。本质上文件是资源所以POST /files没什么不对响应返回文件ID和访问URL。大文件分片上传则是把“上传任务”变成资源POST /upload_tasks创建任务PUT /upload_tasks/xxx/parts上传分片POST /upload_tasks/xxx/complete完成合并。看起来比RPC风格的/uploadBigFile复杂但可恢复、可查询进度其实是更健壮的建模方式。7.5 错误信息怎么写才能帮到前端错误响应除了给机器读的code一定要给人类读的message最好再带一个hint或details字段告诉调用方怎么修。比如“用户名至少6个字符”比“参数错误”有用十倍。同时注意别把服务端内部异常堆栈直接暴露给客户端那既不安全也不利于排查。服务端要做的是把标准化错误码、日志traceId、用户提示文案三层拆开。7.6 查询参数与请求体的边界一个常见边界问题GET能不能带bodyHTTP规范没禁止但实践上通信链路里的代理、缓存组件普遍忽略GET body服务端框架也可能不解析。所以复杂查询一律推荐GET查询参数再复杂就把查询条件建模成“查询任务”资源POST后台异步执行通过任务ID轮询结果这种方案在海量数据导出场景非常好用。7.7 状态机与资源状态设计RESTful接口的易用性很多时候取决于资源状态设计。拿订单举例如果状态字段是一堆无序字符串那前端拿到数据根本不知道下一步能不能取消。正确做法是做显式状态机定义状态集合和允许的转移。设计时可以把状态转移做成资源行为的一部分——比如“支付”在业务上就是让订单从pending_payment变成paid。两种建模思路都行一种是把状态转移作为字段更新PATCH修改status另一种是单独的动作接口POST/orders/10086/pay。前者更REST后者更显式团队按自己的理解水平选择就好关键是文档要写清楚状态流转图。8. 我的实际体会与收尾建议踩过这么多坑之后我个人对RESTful设计的感受是它不是什么高深理论而是一套“约束换取一致性”的工程方案。你牺牲掉随手命名接口的自由换来的是路由清晰、定位直观、前后端沟通成本下降。设计一套接口之前多花半天做资源建模比写三百行Controller然后返工要划算得多。有几个小习惯强烈建议养成第一个新建接口前先对着资源清单问一遍“这个方法对吗、状态码对吗、要不要幂等”第二个把响应结构和错误结构做成团队模板先统一再发挥第三个OpenAPI文件不是文档而是契约前端可以基于它自动生成请求代码后端可以用它做接口测试别让它躺着吃灰。这个内容其实还可以继续往深处聊比如REST与GraphQL、gRPC的对比选型比如超媒体驱动在实际业务里怎么低成本落地。但先把基础这一层扎实了你的接口设计水平已经能超过大多数团队平均水平了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑