Skip to content

URI 设计规范

本章导读:URI 是 REST 契约中最"长寿"的部分——它会出现在书签、日志、第三方系统、爬虫与用户分享里,几乎不可回收。本章从 RFC 3986 的语法底线讲起,给出集合命名、层级、字符选择、查询参数等全套规则,最后附正反例对照表。

1. 先守住语法底线(RFC 3986)

一个 URI 的通用语法:

scheme ":" ["//" authority] path ["?" query] ["#" fragment]

https://api.example.com:443/v1/articles/42?fields=title#section-2
└─scheme─┘ └─────authority─────┘ └──path──┘ └─query─┘ └fragment┘

路径段(path segment)之间以 / 分隔;? 之后是查询串(k=v&k2=v2);# fragment 不会发送到服务器(纯客户端定位),API 设计不要用。

必须遵守的字符规则

类别字符规则
非保留字符A-Z a-z 0-9 - . _ ~URI 设计只应该用这些
保留字符(gen-delims): / ? # [ ] @有结构含义,出现即改变解析
子分隔符! $ & ' ( ) * + , ; =路径段内可用但有歧义,慎用
其它一切中文、空格、%必须百分号编码(UTF-8 后编码),如 空格→%20

推论:

  • 路径参数值可能含特殊字符时,必须编码后放入(/files/2026%2F09%2Freport.pdf),或者改用查询参数承载。
  • 不要在路径里出现 %2F(编码后的斜杠)——很多服务器/网关会先行解码导致路由错乱,含斜杠的 ID 建议走 query 或 base64url
  • + 在查询串中历史上有"空格"歧义(表单编码),路径中无语义——统一用 %20 表示空格最安全。

2. 风格约定(社区共识)

2.1 层级结构

https://{host}/{base-path}/{version}/{resources}/{resource-id}/{sub-resources}/{sub-id}
        │        │        │        │           │
        │        │        │        │           └ 定位单个资源
        │        │        └────────┴ 名词、复数、kebab-case
        │        └ 可选:多服务共享域名时的前缀 /api
        └ 版本(见版本一章)
  • 路径表达"名词的从属",深度 ≤ 2 层资源(见资源与资源层次)。
  • 一切过滤/排序/分页/字段选择放查询参数,不进路径:
    ❌ /articles/author/7/published/true/page/2
    ✅ /articles?authorId=7&status=published&page=2
    理由:路径标识资源,查询参数是"对该资源表示的定制"——这是 RFC 3986 中 path 与 query 的官方分工。

2.2 大小写

推荐全小写 + 连字符(kebab-case):

✅ /user-profiles          ❌ /UserProfiles
✅ /orders/123/items       ❌ /orders/123/getItems

原因:URI 的路径大小写敏感性取决于部署(Windows IIS 默认不敏感、Linux nginx 敏感),混用即事故;连字符对 SEO/可读性最优(Google 指南推荐),下划线在渲染时易被链接下划线淹没。

2.3 复数与单数

  • 集合资源用复数:/articles/articles/42/comments
  • 例外:单例资源(每用户唯一的资源)用单数合理:/users/7/settings/sessions/current/me

2.4 尾斜杠、扩展名、文件式片段

❌ /articles/42/         ← 尾斜杠:统一不使用(团队一致即可,混用会分裂缓存键)
❌ /articles/42.json     ← 内容协商走 Accept 头,不用扩展名
✅ /articles/42          ← 表示格式由媒体类型决定

(Google AIP-127/128 明确禁止 .json 式扩展名;需要多格式时用 Accept 或专门的表示子资源 /articles/42.pdf 这种"资源本身即文件"的例外。)

2.5 查询参数命名

统一 camelCase(与 JSON 字段一致,见响应结构规范):

GET /articles?authorId=7&status=published&sort=-createdAt&page=2&pageSize=20
  • 布尔参数用"值"表达:?published=true(参数名是字段名),不推荐 ?published 裸值或 ?is_published 混乱。
  • 列表值:重复参数(?tag=a&tag=b)或逗号分隔(?tag=a,b)皆可,团队二选一并在 OpenAPI 中声明 explode
  • 参数名用完整单词,缩写只留白名单:iduuidurlurimaxpage

3. 特殊操作的 URI 模式

场景模式
动作无法名词化POST /{resource}/{id}/{action}POST /orders/7/cancel
单例子资源GET/PUT /{resource}/{id}/{singleton}GET /users/7/settings
"当前"语义保留字 current/meGET /users/meGET /sessions/current
自定义方法(Google AIP-136)POST /{resource}/{id}:actionPOST /articles/42:publish
批量POST /{resource}:batchCreate批量操作设计
作业/异步进度GET /{resource}/{id}/jobs/{jobId} 或独立 /jobs/{id}见实战篇

: 是保留字符但允许出现在路径段中(AIP-136 依赖此点);若团队不想引入非常规字符,退回 /{id}/publish 形式。

4. 域名与路径布局

对外公开 API:  https://api.example.com/v1/...          ← 独立子域,便于单独限流/证书/版本发布
多产品线:      https://api.example.com/blog/v1/...      ← 二级路径区分服务
单体应用内:    https://www.example.com/api/v1/...       ← 与页面同域,减少 CORS
  • 永远 HTTPS:HTTP 明文下 Token 与隐私数据等同公开;HSTS 建议开启(详见安全章)。
  • 环境用域名/子域区分(staging.api.example.com),不在 URI 里塞 /test/

5. 正反例对照速查

反例正例违反的点
/getArticleListGET /articles动词、单复数
/api/v1/article/getById?id=1/api/v1/articles/1RPC 式路径
/Articles?Sort_By=CREATED_AT/articles?sort=-createdAt大小写混排
/v1/articles/42/comments/7/replies/3/replies/3(≤2 层规则)层级过深
/articles?page=2&limit=20&offset=0&from=10&size=400固定一套分页参数同义参数并存
/user/7//users/7单数 + 尾斜杠
GET /articles/deleteAll(不存在——删除用 DELETE,批量删除显式设计)动词滥用、用安全方法做删除
/download?file=/etc/passwd资源 ID 化 + 输入校验路径注入(安全反例)

6. URI 的"不可变性契约"

上线的 URI 会被缓存与外部系统集成,把它当 API 的"主键"对待

  1. 路由变更用 301 Moved Permanently(永久)或 308(保留方法)重定向至少一个弃用周期——见 RFC 9110 §15.4。
  2. 301 可能把 POST 变成 GET(历史客户端行为),需要保持方法不变的重定向用 308
  3. 资源永久消失返回 410 Gone,而不是含糊的 404——帮助合法客户端清理失效链接。
  4. 版本升级不改旧 URI(见下一章)。

7. 本章小结

  • 语法底线来自 RFC 3986:非保留字符 + 正确百分号编码;路径标识资源、查询定制表示。
  • 风格三件套:小写 kebab-case、资源复数、无扩展名无尾斜杠——核心是团队一致
  • URI 是持久契约:重定向、弃用、410 是它的新陈代谢机制。

8. 下一步

版本信息放哪里?URI、查询、还是头部? → 版本设计