3. URI 设计规范:资源命名规则、版本控制策略、路径参数与查询参数

聊到 RESTful API,URI 设计绝对是第一个要啃的硬骨头。我见过太多项目,API 上线半年后,光看 URL 就让人头大——有的用动词,有的用复数,版本号塞在 query 里,参数命名全靠心情。说白了,URI 就是你和前端、客户端之间的契约。契约写得烂,后面全是坑。

今天咱们就掰开揉碎,把资源命名、版本控制、参数设计这三块讲透。嗯,都是我在实际项目中踩过的坑,你听听看。

3.1 资源命名规则:名词复数,小写连字符

REST 的核心思想是「资源」。资源就是名词,不是动作。所以 URI 里不应该出现动词。

核心原则:

  • 使用名词复数形式:/users 而不是 /user/getUser
  • 全部小写,单词间用连字符:/order-items 而不是 /orderItems/order_items
  • 避免动词:/orders + POST 就是创建,不需要 /createOrder
  • 层级关系用斜杠表示:/users/{id}/orders

举个例子。你要获取某个用户的订单列表。错误的写法是 /api/getUserOrders?userId=123。正确的写法是 /users/123/orders。你看,一眼就能看懂:用户 123 下面的订单集合。

我个人习惯在项目初期就定好命名规范文档。团队里新来的同事,照着文档写,基本不会跑偏。我曾经在一个项目里看到过 /api/v1/getAllActiveUsersList 这种 URI……嗯,那感觉就像在代码里看到 $dataArray 一样,想重构又不敢动。

小技巧:资源名尽量用业务领域内的通用术语。比如电商项目用 productscartsorders,别自己发明 items_for_sale 这种怪名字。前后端沟通成本会低很多。

3.2 版本控制策略:放在路径里,别放 query 里

API 版本控制是个老生常谈的问题。我见过三种主流做法:

策略 示例 优缺点
路径版本 /api/v1/users 最直观,缓存友好,推荐
请求头版本 Accept: application/vnd.myapp.v1+json URI 干净,但调试麻烦,不直观
查询参数版本 /api/users?version=1 容易混淆,缓存困难,不推荐

我个人强烈推荐路径版本。为什么?

第一,直观。你在浏览器里直接敲 /api/v2/users 就能测试,不需要改请求头。第二,缓存友好。CDN 可以根据路径直接缓存不同版本。第三,迁移成本低。旧版本可以继续运行,新版本并行上线。

我曾经接手过一个项目,版本号放在 query 里:/api/users?v=1.2。结果前端传了个 v=1.2.3,后端没做校验,直接报 500。排查了半天,才发现是版本号格式不统一。从那以后,我坚持用路径版本,简单粗暴,不出幺蛾子。

注意:版本号建议用整数(v1, v2),不要用 v1.1、v2.3 这种小版本。小版本更新应该向后兼容,不需要改 URI。只有不兼容的变更才需要升大版本。

3.3 路径参数:标识具体资源

路径参数用来定位单个资源或资源集合。说白了,就是 URI 里那些花括号里的东西。

// 获取单个用户
GET /users/{id}

// 获取用户的某个订单
GET /users/{userId}/orders/{orderId}

// 嵌套资源
GET /categories/{categoryId}/products/{productId}

路径参数有几个设计要点:

  • 使用唯一标识符:通常是自增 ID 或 UUID。我个人偏爱 UUID,虽然长一点,但防枚举、方便分库分表。
  • 层级不要太深:超过三级嵌套就该考虑简化了。比如 /users/{id}/orders/{orderId}/items/{itemId}/details 这种,读起来都费劲。
  • 参数名要有意义{id} 可以,但最好明确是 {userId} 还是 {orderId},尤其当多个参数混在一起时。

你想想看,如果路径参数叫 {a}{b},前端调用时得猜半天。我见过一个接口,路径是 /api/{type}/{id}/{sub},文档里写「type 是资源类型,id 是主键,sub 是子类型」。结果三个参数都是字符串,前端传错顺序,后端也不校验,数据直接写歪了。嗯,后来重构时我第一个改的就是这个接口。

3.4 查询参数:过滤、排序、分页

查询参数用来对资源集合做「非标识性」的操作。说白了,路径参数是「找哪个」,查询参数是「怎么找」。

常见的查询参数场景:

用途 示例 说明
过滤 ?status=active&category=electronics 按字段筛选
排序 ?sort=created_at&order=desc 指定排序字段和方向
分页 ?page=1&per_page=20 控制返回数量
字段选择 ?fields=id,name,price 只返回需要的字段
搜索 ?q=keyword 全文或模糊搜索

这里有几个设计原则:

  • 参数名用下划线或驼峰? 我个人建议统一用下划线(snake_case),因为 URI 本身不区分大小写,但参数值区分。下划线更清晰。
  • 布尔值用 true/false:不要用 1/0 或 yes/no。前端传 ?is_active=1,你后端还得转一下,何必呢?
  • 数组参数怎么传? 推荐重复参数名:?status=active&status=pending。PHP 里用 $_GET['status'] 直接拿到数组。
  • 分页参数要统一:我习惯用 pageper_page,从 1 开始计数。也有团队用 offsetlimit,从 0 开始。选一种,文档写清楚,别混用。

避坑指南:我曾经在分页接口里同时支持了 pageoffset,想着兼容旧版本。结果前端有的传 page=2,有的传 offset=20,后端逻辑里还要判断优先级。后来我直接废弃了 offset,统一用 page。少一个选择,少一堆 bug。

3.5 综合示例:一个完整的 URI 设计

咱们把上面讲的串起来,看一个完整的例子。

// 获取第2页,每页10条,状态为 active 和 pending 的订单
// 按创建时间降序排列,只返回 id、status、total 字段
GET /api/v1/orders?page=2&per_page=10&status=active&status=pending&sort=created_at&order=desc&fields=id,status,total

这个 URI 清晰表达了:

  • 资源是 orders(名词复数)
  • 版本是 v1(路径版本)
  • 分页、过滤、排序、字段选择都用查询参数

你想想看,如果把这个 URI 拆成多个接口,比如 /api/v1/getActiveOrders/api/v1/getPendingOrders/api/v1/getOrdersSortedByDate……那得定义多少个端点?维护起来得多痛苦?

所以,好的 URI 设计,本质上是在做「抽象」。把资源抽象出来,把操作抽象成 HTTP 方法,把条件抽象成查询参数。这样,一个端点就能覆盖 80% 的业务场景。

总结一下:

  • 资源用名词复数,小写连字符
  • 版本放路径里,用整数
  • 路径参数定位资源,查询参数过滤集合
  • 参数命名统一,类型明确
  • 层级不超过三级,保持简洁

嗯,URI 设计这块,说白了就是「约定大于配置」。定好规范,团队遵守,后面能省 80% 的沟通成本。我见过太多项目,前期图省事随便写,后期重构时哭都哭不出来。所以,从一开始就把 URI 设计好,比什么都重要。

下一章咱们聊聊 HTTP 方法的使用规范。GET、POST、PUT、DELETE 这些,你真的用对了吗?