1. 项目概述:为什么今天还在谈 RESTful API 设计规范?
“RESTful API 设计规范”这八个字,听起来像一份尘封在架构师抽屉底下的老文档,又像大学课堂里PPT上一闪而过的概念。但如果你最近调试过一个返回400 invalid schema for function 'artifact'的接口,或者在 Postman 里反复修改请求体却始终收不到201 Created,又或者被前端同事一句“你这个/getUsers?id=123是不是该改成/users/123?”问得哑口无言——那你不是在复习旧知识,而是在直面每天都在发生的、真实存在的协作断层。
我从2013年开始写第一个 Spring MVC 的@RestController,到后来带团队重构三个不同行业的中台服务,踩过最深的坑从来不是技术选型,而是接口边界模糊带来的连锁反应:前端发错请求不敢改,后端加个字段要同步通知五个人,测试用例写到一半发现路径语义自相矛盾,运维查日志时分不清哪个/v1/user/update是改密码、哪个是改头像……这些都不是代码 bug,而是设计失焦。
RESTful 不是一种技术,而是一套面向人与系统协同的通信契约。它解决的核心问题非常朴素:当十个人(前端、移动端、第三方、测试、产品、运维、甚至未来的你自己)同时盯着同一个接口文档时,能不能不靠口头约定、不靠猜、不靠翻源码,就能准确理解“这个 URL 代表什么资源”、“这个 HTTP 方法到底在表达什么意图”、“出错了我该看哪一行错误码”。
关键词“RESTful”“API”“设计规范”之所以常年霸榜热搜,并非因为大家爱学理论,而是因为每一次不规范的设计,都会在后续两周内以三倍成本返还——多写两版文档、多开三次对齐会、多修五个线上问题。本文不讲 RFC 2616 原文,也不堆砌抽象原则。我会用过去十年在电商、SaaS 和政企项目里打磨出的实操框架,拆解一套真正能落地、能审计、能传承的 RESTful API 设计规范。它不追求教科书式的完美,但保证每一条规则背后都有血泪教训支撑,每一处取舍都经得起生产环境拷问。无论你是刚写完第一个curl -X POST的新手,还是正为微服务网关路由策略头疼的架构师,这里的内容都能直接抄进你的团队 Wiki。
2. 核心设计思路:RESTful 不是语法糖,而是领域建模的外化
2.1 为什么必须从资源建模开始,而不是从 CRUD 操作开始?
很多团队一上来就定规则:“所有接口必须用/v1/{noun}格式”,结果三个月后出现/v1/user/resetPassword、/v1/user/sendVerificationCode、/v1/user/activateAccount——全是动词结尾。这不是格式问题,是建模起点错了。
RESTful 的本质是Resource-Oriented Architecture(ROA),它的第一性原理是:系统对外暴露的,永远是“名词”(资源),而不是“动词”(操作)。HTTP 方法(GET/POST/PUT/PATCH/DELETE)才是承载动作的载体。把“重置密码”设计成/user/resetPassword,等于把业务逻辑硬编码进 URL,既违反幂等性(重复调用 resetPassword 可能产生副作用),又丧失可缓存性(GET 请求本可被 CDN 缓存,但动词路径让缓存策略失效)。
正确做法是建模出PasswordResetToken这个资源:
POST /v1/password-reset-tokens→ 创建一个重置令牌(返回201 Created+ Location 头)GET /v1/password-reset-tokens/{id}→ 查询令牌状态(供前端轮询)PATCH /v1/password-reset-tokens/{id}→ 提交新密码(幂等,多次提交同一密码无副作用)
你看,动作(创建、查询、更新)由 HTTP 方法表达,对象(令牌)由 URL 表达。这种分离让接口具备天然的可组合性:未来加短信验证码校验?只需在POST /v1/password-reset-tokens请求体里增加sms_code字段;加邮箱二次确认?新增email-verification-tokens资源即可,无需改动原有路径。
提示:判断一个 URL 是否符合 RESTful,有个极简测试法——把它读出来,如果能自然接上 “is a” 或 “represents”,就是合格的资源名。例如
/users→ “users is a collection of user resources”;/user/resetPassword→ “user/resetPassword is a... what?动作?函数?这不符合英语语法习惯。”
2.2 版本控制:为什么/v1/必须放在 URL 路径里,而不是 Header 或 Query?
网络热词里频繁出现api error: 400 the supported api model names are deepseek-flash, deepseek-v4,这类错误表面是模型名不匹配,深层常源于版本混乱。比如某次升级后,前端仍调用旧版/users,后端却按新版协议解析请求体,导致 schema 校验失败。
版本控制有三种主流方案:
- URL 路径(如
/v1/users) - Accept Header(如
Accept: application/vnd.myapi.v1+json) - Query 参数(如
/users?version=v1)
我们团队在金融级系统中强制采用路径版本,理由很实际:
- 可追溯性:Nginx 日志、APM 链路追踪、数据库慢查询日志里,
/v1/users和/v2/users天然隔离,排查问题时不用额外解析 Header; - 缓存友好:CDN 和浏览器缓存基于完整 URL,
/v1/users和/v2/users被视为完全不同的资源,避免因缓存污染导致新旧版本混用; - 调试直观:Postman 里一眼看清调用的是哪个版本,前端 Axios 拦截器统一拼接 baseURL 即可,无需为每个请求手动设置 Header。
曾有个项目尝试 Accept Header 方案,结果测试环境里 Chrome 插件自动添加了Accept: */*,覆盖了业务代码设置的v1,导致 30% 接口降级到兼容模式,花了两天才定位。路径版本虽牺牲了一点“语义纯粹性”,但在工程实践中,可观察性 > 理论优雅性。
2.3 状态码:为什么200 OK不是万能钥匙,而422 Unprocessable Entity才是接口工程师的救命稻草?
看到api error: 400 invalid schema for function 'artifact'这类报错,第一反应往往是“参数错了”。但400 Bad Request只表示客户端请求语法错误(如 JSON 格式非法、URL 编码错误),而invalid schema实际属于语义错误——数据格式合法,但业务规则不满足(如手机号少一位、邮箱未验证、枚举值超出范围)。
HTTP 状态码不是装饰品,它是客户端决策的唯一依据:
400→ 前端应检查网络请求是否被代理篡改、JSON 序列化是否出错;422→ 前端应解析响应体中的errors字段,高亮对应表单项;401→ 触发登录态刷新流程;403→ 显示权限不足提示,而非跳转登录页;404→ 区分“资源不存在”和“接口路径错误”,前者可引导用户检查 ID,后者需报警。
我们团队的规范强制要求:所有业务校验失败必须返回422 Unprocessable Entity,且响应体结构统一:
{ "code": "VALIDATION_ERROR", "message": "Validation failed", "details": [ { "field": "email", "reason": "must be a valid email address", "value": "invalid-email" } ] }这套约定让前端 SDK 能自动绑定错误到 UI 组件,测试同学用 Postman 导出的 Collection 可直接生成自动化校验用例。比起泛泛的400,422是给机器看的精准信号,更是给开发者省下 80% 错误处理时间的基础设施。
3. 关键细节解析:从 URL 命名到错误处理的实战守则
3.1 URL 设计:名词复数、连字符、层级嵌套的取舍逻辑
URL 是接口的第一张名片,它必须让人“望文生义”。我们团队执行三条铁律:
第一,资源名必须用复数名词,且为小写连字符分隔(kebab-case)
- ✅
/v1/product-categories - ✅
/v1/shipping-addresses - ❌
/v1/productCategory(单数易误解为单个实例) - ❌
/v1/ProductCategories(大小写混用在某些代理服务器中可能被转义) - ❌
/v1/product_categories(下划线在部分 CDN 或日志系统中会被过滤)
为什么坚持复数?因为/v1/users天然表达“用户集合”这一资源,而GET /v1/users/123是其子资源。若用单数/v1/user,GET /v1/user/123就成了“获取 user 的 123 子资源”,语义断裂。复数形式让集合操作(GET /v1/users)和个体操作(GET /v1/users/{id})形成自然映射。
第二,嵌套层级不超过两层,禁止深度路径
- ✅
/v1/users/{user_id}/orders(用户→订单,合理) - ✅
/v1/orders/{order_id}/items(订单→商品项,合理) - ❌
/v1/users/{user_id}/orders/{order_id}/items/{item_id}/reviews(四层嵌套,难以维护,且违背单一职责)
深度嵌套看似体现关系,实则制造耦合。/v1/users/123/orders/456/items/789/reviews这种路径,一旦订单取消,/v1/orders/456本身已不存在,其子资源更无意义。正确做法是扁平化设计:/v1/reviews?order_id=456&item_id=789,用查询参数表达可选约束,主资源/v1/reviews保持独立生命周期。
第三,避免动词,但允许极少数经过共识的“伪资源”
绝对禁止/v1/users/activate,但允许/v1/users/{id}/activation——这里activation是一个资源(激活状态),而非动作。POST /v1/users/123/activation创建激活记录,GET /v1/users/123/activation查询当前状态。这种设计让“激活”行为可审计(谁在何时激活)、可重放(重发激活邮件)、可撤销(DELETE /v1/users/123/activation)。
注意:所谓“伪资源”必须满足两个条件:1)有明确的生命周期(可创建、可查询、可删除);2)业务上存在状态实体(如激活码、支付凭证、审批单)。不能为了绕开规则而生造名词。
3.2 HTTP 方法语义:PUT 与 PATCH 的生死线,以及为什么 DELETE 不该返回 200
HTTP 方法不是按钮标签,它们定义了服务器必须保证的语义契约。用错一个方法,轻则让前端无法正确缓存,重则引发数据不一致。
PUT vs PATCH:这是 RESTful 最常被误用的分水岭
PUT /v1/users/123:全量替换。客户端必须发送完整的用户对象(包括 name、email、phone、avatar_url 等所有字段),服务器用新数据完全覆盖旧数据。若客户端漏传avatar_url,该字段将被置空。PATCH /v1/users/123:局部更新。客户端只发送需要修改的字段(如{ "email": "new@example.com" }),服务器仅更新指定字段,其余保持不变。
我们团队的规范强制:所有更新接口默认使用 PATCH。原因很现实:前端表单往往只修改部分字段(改邮箱不改头像),若强制 PUT,前端必须先GET /v1/users/123拉取全量数据,再合并修改后提交,徒增一次网络往返和并发风险(A 用户 GET 后,B 用户修改了 name,A 提交时会覆盖 B 的修改)。
只有两种场景用 PUT:
- 资源创建:
PUT /v1/users/123(当客户端能生成唯一 ID 时,如 UUID); - 幂等性要求极高:如银行转账指令,必须确保“提交相同请求体,结果完全一致”,此时 PUT 的全量语义反而更安全。
DELETE 的陷阱:为什么它必须返回 204 No Content,而非 200 OK?DELETE /v1/users/123成功后,资源已不存在。若返回200 OK并附带用户数据(如{ "id": 123, "name": "John" }),会产生逻辑悖论:既然资源已被删除,返回的数据从何而来?是缓存?是软删除?客户端无法判断状态。
标准做法是204 No Content——明确告知“操作成功,且无内容返回”。前端收到 204,直接从本地列表移除该项即可,无需解析响应体。若业务需要返回删除摘要(如“已删除 1 个用户,关联 5 条订单”),则用200 OK+ 明确的summary字段,但必须在规范中单独定义,不可作为 DELETE 的默认行为。
3.3 错误处理:构建可编程的错误体系,终结400泛滥
api error: 400 invalid schema for function 'artifact'这类错误,根源在于错误分类颗粒度太粗。一个400要覆盖语法错误、参数缺失、类型错误、业务规则冲突等十几种场景,前端只能弹窗“请求失败”,用户不知所措。
我们团队推行三级错误体系:
| 错误层级 | HTTP 状态码 | 触发场景 | 响应体要求 | 前端处理建议 |
|---|---|---|---|---|
| 语法层 | 400 Bad Request | JSON 解析失败、URL 编码错误、Content-Type 不匹配 | {"code":"PARSE_ERROR","message":"Invalid JSON"} | 记录原始请求,提示“网络异常,请重试” |
| 语义层 | 422 Unprocessable Entity | 参数校验失败(长度、格式、枚举)、业务规则冲突(余额不足、库存为零) | {"code":"INSUFFICIENT_BALANCE","message":"Balance is insufficient","details":[{"field":"amount","value":"100"}]} | 解析details字段,高亮表单项,显示具体错误文案 |
| 授权层 | 401 Unauthorized/403 Forbidden | Token 过期、权限不足、租户隔离失败 | {"code":"TOKEN_EXPIRED","message":"Authentication token expired"} | 401触发登录态刷新;403显示权限提示,不跳转 |
关键实践:
- 错误码(code)必须全局唯一且可读:禁用
ERR_001这类数字码,用USER_NOT_FOUND、ORDER_STATUS_INVALID等语义化字符串,方便日志搜索和监控告警; - message 专供用户阅读,details 专供程序解析:
message用中文短句(如“用户不存在”),details包含field(出错字段名)、value(用户输入值)、reason(校验规则描述),前端可据此做精细化交互; - 所有错误响应体结构必须严格一致,哪怕是最简单的
401,也要返回{"code":"UNAUTHORIZED","message":"Login required"},避免前端写多个错误处理分支。
曾有个项目因错误体结构不统一,前端写了 7 个if (res.status === 400) { ... } else if (res.data.code === 'VALIDATION_ERROR') { ... }分支,每次后端加一个新错误码,前端就要改代码。统一结构后,SDK 封装一层通用错误处理器,新增错误码只需在配置表里加一行。
3.4 分页与过滤:为什么?page=1&size=20是反模式,而游标分页才是高并发答案
restful接口对接场景下,分页是最易被忽视的性能雷区。?page=1&size=20看似简单,但在千万级数据表中,SELECT * FROM orders ORDER BY created_at DESC LIMIT 40000,20会导致 MySQL 扫描前 40020 行,响应时间从 20ms 暴涨到 2s。
我们团队在 C 端高并发场景(如商品列表)强制使用游标分页(Cursor-based Pagination):
GET /v1/products?cursor=abc123&limit=20- 响应体包含
next_cursor字段,用于下一页请求
游标本质是上一页最后一条记录的排序字段值(如created_at时间戳 +id),数据库用WHERE created_at < '2023-01-01' AND id < 1000快速定位,避免OFFSET的全表扫描。
但游标分页不适用于所有场景:
- 管理后台:运营人员需要跳转到第 100 页看历史数据,游标无法满足,此时用
?page=100&size=20+ 数据库索引优化(如created_at单独建索引); - 搜索结果:Elasticsearch 的
from/size在深分页时同样低效,应改用search_after(ES 的游标机制)。
过滤参数设计同样讲究:
- ✅
?status=active,inactive&category_id=1,2,3(逗号分隔,语义清晰) - ✅
?price_min=100&price_max=500(范围查询,字段名直白) - ❌
?q={"status":["active","inactive"]}(把复杂结构塞进 query,破坏 URL 可读性和缓存)
所有过滤参数必须有明确的文档说明其支持的操作符(eq、in、gt、lt),并在 Swagger 中标注allowEmptyValue = false,避免前端传空字符串导致 SQL 注入风险。
4. 实操落地:从规范文档到团队执行的完整闭环
4.1 规范文档:如何写出一份让开发、测试、前端都愿意看的说明书?
一份好的规范文档,不是写给架构师看的,而是写给明天要写接口的 junior dev 看的。我们团队的文档模板包含四个必填模块:
1. 资源概览表(Resource Overview)
用表格列出所有核心资源,明确其生命周期和权限:
| 资源路径 | HTTP 方法 | 描述 | 认证要求 | 幂等性 | 示例请求 |
|---|---|---|---|---|---|
/v1/users | GET | 获取用户列表 | JWT Bearer | 是 | curl -H "Authorization: Bearer xxx" https://api.example.com/v1/users?status=active |
/v1/users | POST | 创建新用户 | JWT Bearer | 是 | curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' ... |
2. 请求/响应契约(Contract)
每个接口单独一页,包含:
- 请求体 Schema:用 OpenAPI 3.0 定义,标注必填/可选、数据类型、示例值;
- 响应体 Schema:区分
200、422、401等状态码的响应结构; - 错误码字典:列出该接口可能返回的所有
code值及含义; - 性能 SLA:P95 响应时间 ≤ 200ms,超时时间 5s。
3. 常见场景速查(Quick Reference)
用问答形式解决高频困惑:
- Q:如何修改用户邮箱?
A:PATCH /v1/users/{id},请求体{"email": "new@example.com"},需提供X-Original-EmailHeader 校验原邮箱。 - Q:删除用户后,其订单是否自动取消?
A:否,订单是独立资源,需调用PATCH /v1/orders/{id}更新状态。
4. 工具链集成(Tooling)
明确规范如何落地:
- Swagger UI 自动生成文档,URL 公布在团队 Wiki;
- 使用
openapi-generator从 OpenAPI 文件生成 TypeScript 客户端 SDK,前端直接npm install @myapi/sdk; - CI 流程中加入
spectral工具校验 OpenAPI 文件是否符合规范(如路径是否含动词、状态码是否缺失)。
实操心得:文档初稿完成后,必须找一名没参与设计的 junior dev 试读。让他用文档独立完成一个接口开发,记录所有看不懂、找不到、有歧义的地方。我们曾发现“
status字段支持active/inactive/pending”这句话,新人以为pending是后端生成的中间态,实际是前端提交的初始值,最终在文档里加了注释:“pending仅用于新用户注册流程,由前端在POST /v1/users时指定”。
4.2 代码实现:Spring Boot 下的规范落地技巧
规范的生命力在于能否低成本执行。我们在 Spring Boot 项目中通过以下方式降低落地门槛:
1. 全局异常处理器(Global Exception Handler)
统一捕获所有异常,转换为标准化错误响应:
@RestControllerAdvice public class RestExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation( MethodArgumentNotValidException ex) { List<ErrorDetail> details = ex.getBindingResult() .getFieldErrors().stream() .map(error -> ErrorDetail.builder() .field(error.getField()) .value(String.valueOf(error.getRejectedValue())) .reason(error.getDefaultMessage()) .build()) .collect(Collectors.toList()); return ResponseEntity.unprocessableEntity() .body(ErrorResponse.builder() .code("VALIDATION_ERROR") .message("Validation failed") .details(details) .build()); } }这样,所有@Valid校验失败自动返回422,无需每个 Controller 重复写。
2. 资源路径生成器(Resource Link Builder)
避免硬编码 URL,用 HATEOAS 生成可发现链接:
@GetMapping("/v1/users/{id}") public ResponseEntity<UserResource> getUser(@PathVariable Long id) { User user = userService.findById(id); UserResource resource = UserResource.from(user); // 自动添加相关链接 resource.add(linkTo(methodOn(UserController.class).getUser(id)).withSelfRel()); resource.add(linkTo(methodOn(OrderController.class).getOrdersByUserId(id)).withRel("orders")); return ResponseEntity.ok(resource); }前端通过_links.orders.href获取订单列表 URL,无需记住/v1/users/{id}/orders路径,为未来路径调整留出余地。
3. 版本路由拦截器(Version Router)
用 Spring MVC 的@RequestMapping路径变量统一处理版本:
@RestController @RequestMapping("/v{version:\\d+}") public class UserController { @GetMapping("/users") public List<User> listUsers(@PathVariable String version) { // 根据 version 参数路由到不同实现 return versionService.handle(version, () -> userService.list()); } }比为每个版本建包更轻量,且版本号在日志中天然可见。
4.3 团队协作:如何让规范不沦为墙上的风景画?
再好的规范,没有执行机制就是废纸。我们团队建立三层保障:
第一层:设计评审(Design Review)
任何新接口上线前,必须通过小组评审。评审 checklist 包含:
- [ ] URL 是否为复数名词?是否含动词?
- [ ] HTTP 方法是否符合语义?(如创建用 POST,非 PUT)
- [ ] 错误码是否精确?(
422用于业务校验,非400) - [ ] 分页方案是否匹配场景?(游标 or 页码)
- [ ] OpenAPI 文档是否已更新并生成 SDK?
评审不是走过场,而是现场用 Postman 调试,验证响应体结构、状态码、Header 是否符合约定。
第二层:自动化门禁(CI Gate)
在 GitLab CI 中加入:
openapi-diff工具检测 OpenAPI 文件变更,若新增400状态码而未在文档中说明,流水线失败;swagger-codegen生成客户端 SDK,编译通过才允许合并;curl -I检查所有GET接口是否返回Cache-Control: public, max-age=300(5 分钟缓存)。
第三层:可观测性兜底(Observability)
在 Grafana 中建立规范健康度看板:
400状态码占比 > 5%?→ 检查前端 SDK 是否未处理422;200响应中content-length为 0 的比例?→ 发现未按规范返回204的 DELETE 接口;- 某个
POST接口 P95 > 1s?→ 触发告警,检查是否遗漏了@Transactional或 N+1 查询。
注意事项:规范推广初期,切忌一刀切。我们允许老接口逐步迁移,但所有新功能、新模块必须 100% 符合。用“新项目强制,老项目自愿”的策略,比强行改造引发抵触更有效。曾有个遗留系统,我们花了三个月,只把用户中心模块迁移到新规范,用实际效果(前端联调时间减少 40%,线上 4xx 错误下降 65%)说服了其他团队。
5. 常见问题与排查技巧实录:那些年我们踩过的坑
5.1 问题速查表:从报错信息反推设计缺陷
| 报错现象 | 可能根源 | 排查步骤 | 解决方案 |
|---|---|---|---|
api error: 400 invalid schema for function 'artifact' | 1. 请求体 JSON 结构与 OpenAPI 定义不符 2. 字段类型错误(如 string 传了 number) 3. 必填字段缺失 | 1. 对比请求体与 OpenAPIcomponents.schemas.Artifact定义2. 检查 Content-Type: application/json是否缺失3. 用 curl -v查看完整请求头和体 | 1. 更新 OpenAPI 定义,或修正前端请求体 2. 后端增加宽松解析(如 Jackson 的 @JsonCreator) |
login failed. check api token or gitlab version. | 1. Token 过期或格式错误 2. /login接口未按规范返回401,而是400 | 1. 检查 Token 签名是否有效(JWT.io) 2. 查看 /login接口响应状态码和WWW-AuthenticateHeader | 1. 强制/login返回401+WWW-Authenticate: Bearer2. 前端 SDK 统一处理 401刷新 Token |
failed to connect to the docker api at npipe:////./pipe/docker_engine | 1. Docker Desktop 未运行 2. Windows WSL2 与 Docker Desktop 冲突 | 1. 运行docker info检查连接2. 在 WSL2 中执行 export DOCKER_HOST=tcp://localhost:2375 | 此问题与 RESTful 规范无关,属本地开发环境配置,需在团队 Wiki 中单独说明 |
api call failed after 3 retries: http 500 | 1. 后端未捕获异常,抛出5002. 重试逻辑未区分可重试错误(如网络超时)与不可重试错误(如业务逻辑异常) | 1. 查看后端日志,确认异常堆栈 2. 检查重试策略是否对 500盲目重试 | 1. 全局异常处理器捕获RuntimeException,返回500+code: INTERNAL_ERROR2. 前端重试只针对 503、504、网络超时,500直接上报 |
5.2 真实案例:一次400错误引发的全链路规范重构
去年某 SaaS 产品上线新功能,前端调用/v1/artifacts创建资源时,频繁收到400 invalid schema for function 'artifact'。起初以为是前端传参问题,但排查发现:
- 前端请求体完全符合 OpenAPI 定义;
- 后端日志显示
JsonMappingException: Can not construct instance of Artifact; - 深入调试发现,OpenAPI 定义中
Artifact的type字段是string枚举,但后端 Java 类用了enum ArtifactType,Jackson 默认反序列化失败。
根因是规范未覆盖数据传输对象(DTO)与领域模型的映射规则。我们立即做了三件事:
- 补充规范:在“数据契约”章节增加“DTO 字段命名必须与 OpenAPI 一致,枚举值必须用
@JsonValue注解导出字符串”; - 工具加固:在 CI 中加入
jackson-databind版本锁,并用openapi-generator生成 DTO 类,禁止手写; - 文档更新:在 Swagger UI 中为
type字段添加example: "document",并注明“枚举值:document, image, video”。
这次事故让我们意识到:RESTful 规范不仅是 URL 和状态码,更是前后端数据流的完整契约。现在,我们要求所有新接口的 OpenAPI 文件必须通过openapi-validator校验,且生成的 DTO 类必须被单元测试覆盖。
5.3 高频避坑指南:那些文档里不会写的实战经验
坑一:PATCH请求体的空值陷阱
前端用PATCH /v1/users/123修改邮箱,请求体{ "email": null }。后端若直接user.setEmail(request.getEmail()),邮箱会被设为null,但业务上null和“不修改”是两回事。
✅ 正确做法:用Optional<String>包装请求字段,或约定null表示“忽略此字段”,""表示“清空此字段”。
坑二:GET请求的缓存穿透GET /v1/users/123返回404,CDN 缓存了这个 404 响应。当真实用户123注册后,首次请求仍返回 404。
✅ 解决方案:对404响应设置Cache-Control: no-store,禁止缓存;或用stale-while-revalidate策略,允许短暂返回过期 404。
坑三:时间戳时区混乱
前端传created_at: "2023-01-01T00:00:00",后端解析为2023-01-01 00:00:00 UTC,但业务要求是“用户本地时区”。
✅ 统一约定:所有时间戳必须带时区(2023-01-01T00:00:00+08:00),后端存储为 UTC,前端展示时按本地时区格式化。
坑四:DELETE的软删除幻觉
为保留审计日志,团队用is_deleted = true实现软删除,但GET /v1/users/123仍返回该用户,只是隐藏了敏感字段。这违反了 RESTful 原则——资源已逻辑删除,就不该再通过GET访问。
✅ 正确做法:DELETE /v1/users/123后,GET /v1/users/123必须返回404 Not Found;审计需求通过独立的/v1/audit-logs?resource_type=user&resource_id=123满足。
这些坑,每一个都来自真实项目的深夜告警电话。它们不会出现在 RFC 文档里,但却是决定接口能否稳定运行的关键细节。
6. 规范演进:当 DeepSeek、Gemini 等大模型 API 成为新基础设施
网络热词中频繁出现deepseek api,gemini api,openai api key,这预示着一个新现实:AI 服务正成为和数据库、消息队列同等重要的基础设施。RESTful API 规范必须适应这一变化,而非固守传统。
我们团队已启动“AI Native API”子规范,核心调整有三点:
1. 资源建模升级:从静态数据到动态能力
传统 API 的/v1/users是静态资源,而POST /v1/chat/completions是动态计算过程。新规范将 AI 接口归类为Action Resources(动作资源):
POST /v1/chat-completions→ 创建一次对话完成任务(返回201 Created+Location: /v1/chat-completions/{id}