RESTful API设计:资源命名、状态码规范、分页设计、版本管理、HATEOAS

聊到RESTful API设计,我脑子里第一个蹦出来的词其实是「约定」。说白了,后端API就是前后端之间的合同。合同写得含糊,后面全是扯皮。我在项目里见过太多因为API设计不规范导致的返工,嗯,今天咱们就把这块掰开揉碎了讲清楚。

核心原则:RESTful API不是写代码,是设计资源。你设计的每一个端点,都应该是对资源的操作,而不是对功能的调用。

1. 资源命名:别让URL变成函数名

我个人习惯把资源命名当成数据库表名来思考。名词、复数、小写、连字符分隔——这是铁律。

// 好的命名
GET /api/users
GET /api/users/123
GET /api/users/123/orders
POST /api/users

// 糟糕的命名
GET /api/getUserList
GET /api/user?id=123
POST /api/createNewUser
GET /api/userOrders?userId=123

为什么不能用动词?你想想看,HTTP方法本身就是动词。GET就是获取,POST就是创建,PUT就是全量更新,PATCH就是部分更新,DELETE就是删除。你在URL里再写一遍动词,等于说废话。

我曾经在一个遗留项目里看到过 /api/deleteUserById 这种端点,结果前端传参传错了,删了不该删的数据。嗯,从那以后我对命名规范格外敏感。

HTTP方法 资源路径 语义
GET /users 获取用户列表
GET /users/42 获取单个用户
POST /users 创建用户
PUT /users/42 全量更新用户
PATCH /users/42 部分更新用户
DELETE /users/42 删除用户

小技巧:资源嵌套不要超过三层。超过三层就该考虑是不是设计有问题了。比如 /users/123/orders/456/items/789 这种,建议拆成 /orders/456/items/789 加上用户ID作为查询参数。

2. 状态码规范:别什么都返回200

我见过最离谱的事,是有人把所有请求都返回200,然后在响应体里塞一个 {"code": 1, "message": "error"}。这等于把HTTP协议当传输层用,自己重新造了一套状态码。何必呢?

HTTP状态码已经定义得很清楚了,直接用就好:

  • 200 OK:GET成功、PUT成功、PATCH成功
  • 201 Created:POST创建资源成功
  • 204 No Content:DELETE成功,不需要返回内容
  • 400 Bad Request:参数校验失败、请求体格式错误
  • 401 Unauthorized:未认证,比如没传token
  • 403 Forbidden:已认证但没权限
  • 404 Not Found:资源不存在
  • 409 Conflict:资源冲突,比如重复创建
  • 422 Unprocessable Entity:语义错误,比如邮箱格式不对
  • 500 Internal Server Error:服务端异常

注意:别滥用500。很多团队把参数校验失败也返回500,这是不对的。客户端传错参数是客户端的锅,应该返回4xx。只有服务端自己炸了才用5xx。

3. 分页设计:别让用户一次拿太多

不分页的API,说白了就是定时炸弹。数据量小的时候没事,等用户量上来了,一次查询几万条记录,数据库扛不住,网络带宽也扛不住。

我个人推荐基于游标的分页,而不是传统的页码分页。为什么?

  • 页码分页GET /users?page=2&size=20。简单直观,但有个致命问题——如果中间有人插入了数据,翻页时会出现重复或遗漏。
  • 游标分页GET /users?cursor=eyJpZCI6MTAwfQ==&limit=20。基于上次返回的最后一条记录的ID,性能稳定,不会受数据变动影响。
// 游标分页响应示例
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "has_more": true,
    "total": 1000
  }
}

我曾经在一个电商项目里用过页码分页,结果用户在下单时翻页,后台新增了商品,导致用户看到重复数据,体验极差。后来全改成游标分页,再没出过问题。

建议:如果数据量不大(比如几千条),页码分页也够用。但如果你做的是社交、电商这类高频写入的系统,游标分页是更稳妥的选择。

4. 版本管理:别让旧客户端炸掉

API一旦发布出去,你就失去了对客户端的完全控制。用户可能用着三年前的App,你总不能强制所有人升级吧?

版本管理有三种常见方式:

  • URL路径版本/api/v1/users/api/v2/users。最直观,但会污染URL。
  • 请求头版本Accept: application/vnd.myapp.v1+json。更符合RESTful风格,但调试起来麻烦。
  • 查询参数版本/api/users?version=1。简单但容易被忽略。

我个人习惯用URL路径版本。虽然不够「纯粹」,但胜在直观。前端看一眼URL就知道调的是哪个版本,后端路由也容易处理。

// Ktor中版本路由示例
routing {
    route("api/v1") {
        get("/users") { /* v1逻辑 */ }
    }
    route("api/v2") {
        get("/users") { /* v2逻辑 */ }
    }
}

避坑指南:我曾经见过一个团队,版本号直接写在代码里,每次发版都要改代码。正确的做法是把版本号作为配置项,通过环境变量或配置文件注入。这样同一个代码包可以部署成v1和v2两个服务。

5. HATEOAS:让API自己「说话」

HATEOAS全称是「Hypermedia as the Engine of Application State」。名字很唬人,说白了就是:API响应里带上链接,告诉客户端下一步能做什么。

举个例子,你查询一个订单,响应里不光有订单数据,还有「支付」「取消」「查看物流」这些操作的链接。客户端不需要硬编码URL,跟着链接走就行了。

{
  "order_id": "ORD-2024-001",
  "status": "pending_payment",
  "total": 299.00,
  "_links": {
    "self": { "href": "/api/v1/orders/ORD-2024-001" },
    "pay": { "href": "/api/v1/orders/ORD-2024-001/payments", "method": "POST" },
    "cancel": { "href": "/api/v1/orders/ORD-2024-001", "method": "DELETE" }
  }
}

这样做的好处是什么?客户端不需要知道业务逻辑。订单状态变了,链接也跟着变。比如订单已经支付了,响应里就不会再出现「pay」链接,而是出现「refund」链接。客户端只需要渲染这些链接就行。

嗯,说实话,HATEOAS在实际项目中用得不算多。因为大部分前端框架还是习惯硬编码URL。但如果你做的是开放平台,给第三方开发者用,HATEOAS能大大降低对接成本。

我的建议:不用全盘HATEOAS,但可以在关键资源上加上链接。比如订单详情、用户资料这些经常被引用的资源,带上关联资源的链接,能让API更「自解释」。

知识体系总览

下面这张图把RESTful API设计的五个核心维度串起来了。你可以把它当成设计API时的检查清单:

RESTful API 设计核心维度 RESTful API 资源命名 名词复数 / 小写连字符 状态码规范 2xx成功 / 4xx客户端错 分页设计 游标分页 / 页码分页 版本管理 URL路径 / 请求头 / 参数 HATEOAS 超媒体驱动 / 自描述API 五个维度相互独立,但共同决定API的质量

这五个维度,说白了就是API设计的「五件套」。资源命名决定了API好不好读,状态码决定了API好不好调,分页决定了API能不能扛住量,版本管理决定了API能不能持续迭代,HATEOAS决定了API能不能自我解释。

我在项目里见过只做前两点的团队,也见过五件套全上的团队。说实话,没有绝对的对错,关键看你的业务场景。但有一点是确定的——规范越早定越好。等API已经上线了再改,那成本可就高了。

总结一句话:好的API设计,是让调用者不用看文档也能猜个八九不离十。如果你设计的API需要别人反复来问你「这个参数是什么意思」,那说明设计还有优化空间。


公众号:蓝海资料掘金营,微信deep3321