上一篇我们建立了”方法要有语义、状态码要准确”的意识。这一篇往更细的地方钻:URL 和资源到底怎么设计。URL 是 API 的脸面,脸面脏了,别人用起来就别扭。
你一定见过这种接口:/getUserList、/updateUserInfo、/api/action?type=delUser。这是典型的”RPC 思维”——把函数名直接翻译成路径。REST 思维则相反:路径只描述”资源是什么”,动作交给 HTTP 方法。这一篇我们把”资源建模 + URL 规范”讲透。
第一步:先想清楚”资源”是什么
设计 API 前,先别急着写路径,而是列出系统里的”名词”——用户、订单、文章、评论、标签。这些名词就是资源。一条铁律:
URL 里只放名词(资源),不放动词(动作)。动作由 HTTP 方法表达。
错误示范(动词进了路径):
/getAllUsers
/createOrder
/deleteCommentById
正确示范(名词 + 方法):
GET /users
POST /orders
DELETE /comments/{id}
判断标准很简单:把路径读出来,如果像”一个东西”,就对了;如果像”一个函数调用”,就错了。
用复数,且全站统一
资源用复数还是单数?社区主流是复数(/users 而不是 /user)。原因:一个集合的视角更自然——“获取用户”通常是获取”用户们”这个集合里的东西。而且要保持全站一致,别这个接口用 /user/1、那个用 /orders,前后矛盾最折磨人。
GET /users # 用户集合
GET /users/1 # 集合里的某一个
层级关系:用嵌套表达”从属”
资源之间有从属关系时,用路径嵌套表达。比如”用户发的文章”:
GET /users/123/articles # 用户 123 下的所有文章
GET /users/123/articles/456 # 用户 123 的文章 456
但嵌套别超过两层,否则路径又长又难读,而且容易让”父资源”变成必填参数(你想查某篇文章,非得知道它属于哪个用户?不一定)。深层关系更推荐”平铺 + 查询参数”:
GET /articles/456 # 直接按文章 id 查
GET /articles?author=123 # 用参数筛选作者
经验法则:一层从属用嵌套(/users/123/orders),跨多层的关联用查询参数过滤,别把路径写成俄罗斯套娃。
过滤、排序、分页:都交给查询参数
列表接口几乎都要”筛选 + 排序 + 翻页”,这些不是资源,是操作资源的”视图条件”,所以放在 ? 后面最合适:
GET /articles?status=published&author=123&sort=-created_at&page=2&size=20
约定俗成的写法:
- 过滤:
?status=published、?author=123(字段=值)。 - 排序:
?sort=created_at(升序)、?sort=-created_at(降序,加负号)。 - 分页:
?page=2&size=20(第 2 页,每页 20 条),或?offset=40&limit=20。 - 字段投影:
?fields=id,title(只要这两个字段,省流量)。
后端拿到这些参数后,翻译成 SQL 的 WHERE、ORDER BY、LIMIT 即可。这样 URL 干净,前端拼条件也灵活。
版本:给接口留条”退路”
接口一旦发布,就有人依赖它。但你迟早要改字段、改逻辑。怎么做到”改了新用户用新的,老用户还能用老的”?答案是版本控制。常见三种做法:
- URL 路径版本(最直观,最常用):
简单粗暴,一眼看出版本,网关也好路由。缺点是路径里永远挂着版本号。/v1/users /v2/users - 请求头版本:
Accept: application/vnd.myapi.v2+json。URL 干净,但调试不直观,得靠工具加头。 - 子域名版本:
https://v2.api.example.com/users。适合大版本隔离。
新手推荐路径版本,最省心。记住:版本是”兼容性契约”,不是装饰——加了 v2 就要保证 v1 在一段时间内还能用。
返回结构:列表和详情要一致
列表接口别只返回裸数组,套一层对象更利于扩展(以后加 total、page 不用改结构):
{
"data": [ { "id": 1, "title": "..." }, { "id": 2, "title": "..." } ],
"page": 1,
"size": 20,
"total": 135
}
详情接口返回单个对象,但字段结构要和列表里的元素一致,前端不用为两种形状写两套解析逻辑。
HATEOAS:让响应”自带下一步”
REST 成熟度模型的最高一级叫 HATEOAS(超媒体驱动)——响应里不仅给数据,还告诉你”接下来能做什么”。比如:
{
"id": 123,
"name": "小明",
"links": [
{ "rel": "self", "href": "/users/123" },
{ "rel": "orders", "href": "/users/123/orders" }
]
}
客户端看了就知道”点 orders 链接能查他的订单”。实际项目里全量 HATEOAS 用得少(前端通常硬编码路由),但”在响应里附上相关资源的链接”这个思路值得借鉴,能减少前后端对 URL 的口头约定。
常见新手坑
- 路径里塞动词:
/getUser这种,破坏了”资源即名词”的原则,方法语义也乱了。 - 嵌套过深:
/a/b/c/d四级嵌套,既难读又强制依赖父资源,用参数过滤替代。 - 分页不返回 total:前端没法画页码,体验差;至少返回
total和page。 - 版本号乱加:今天 v1 明天 v2 又不兼容,老调用方全崩。版本是契约,改前想清楚兼容性。
- 字段命名风格不统一:一会
userName一会user_name,前端解析要写兼容。全站统一用snake_case或camelCase之一。 - ID 用自增整数暴露业务量:
/orders/100001暗示你有一百万单,敏感场景用 UUID 或无规律字符串。
实战:设计一个”博客系统”的接口集
资源有:用户 users、文章 articles、评论 comments、标签 tags。合理设计:
GET /v1/articles?status=published&tag=go&sort=-created_at&page=1
GET /v1/articles/{id}
POST /v1/articles
PUT /v1/articles/{id}
DELETE /v1/articles/{id}
GET /v1/articles/{id}/comments
POST /v1/articles/{id}/comments
GET /v1/users/{id}
GET /v1/tags
观察:资源都是复数名词;动作靠方法;从属关系(文章下的评论)用一层嵌套;列表带过滤排序分页;全站带 /v1 版本。这套接口交给前端,基本不用再解释。
新手怎么把 URL 设计练好
URL 设计是”审美 + 规范”的结合,多看优秀案例进步最快。建议去读 GitHub API、Stripe API 的文档——它们把 REST 规范做到了极致,是活教材。每天挑一个你熟悉的系统(购物车、相册、通讯录),试着用”名词 + 方法 + 参数”的方式把它的接口设计出来,再对照标准挑自己的毛病。
另一个心法:为”未来的你”设计。今天你觉得 /users 够用,明天要按部门筛、要分页、要投影字段。所以一开始就把过滤、排序、分页的参数约定写好,比事后打补丁优雅得多。URL 一旦对外,改起来成本极高,前期多想十分钟,后期少加十天班。
还有,保持”一致性”比”完美”更重要。全站统一复数、统一 camelCase、统一错误结构、统一版本策略,哪怕某个细节不是最优,一致的性能让团队少踩坑。规范的存在,就是为了消灭”每个人随意发挥”带来的混乱。
小测验:看看你掌握了没
- 问题一:为什么 URL 里应该用名词而不是动词?答案:REST 中路径描述”资源是什么”,动作由 HTTP 方法表达;动词进路径会变成 RPC 风格,丢失语义。
- 问题二:资源嵌套最多建议几层?更深的关系怎么处理?答案:建议不超过两层;更深的关联用查询参数过滤(如
?author=123),别套娃。 - 问题三:列表接口为什么要套一层对象而不是直接返回数组?答案:便于扩展
total/page等元信息,且前后结构稳定。
这一篇你该记住的
- URL 只放资源(名词),动作交给 HTTP 方法;用复数且全站统一。
- 从属关系用一层嵌套,深层关联用查询参数过滤。
- 过滤
?status=x、排序?sort=-field、分页?page&size都走查询参数。 - 版本控制留退路,新手推荐路径版本
/v1/...。 - 列表响应套对象返回
data/total/page,字段命名全站统一。 - 一致性 > 完美;URL 对外后改造成本极高,前期多规划。
下一篇我们讲 认证与授权:API 怎么知道”你是谁”、怎么判断”你能不能干这事”,JWT、API Key、OAuth 到底怎么选。