Rest Api设计规范
资源命名、HTTP 方法、URI、分页、排序、过滤
[Requirement] 资源使用名词复数形式,全部小写,单词之间用连字符(kebab-case),不使用动词。
[Standard] 资源层级不超过 3 级(/a/{aId}/b/{bId}/c/{cId}),超过时通过过滤参数或独立资源替代。
| 资源 | 正确 | 错误 |
|---|---|---|
| 用户集合 | /users | /user、/getUsers、/UserList |
| 用户详情 | /users/{userId} | /getUserById |
| 用户订单 | /users/{userId}/orders | /getUserOrders |
| 订单商品 | /orders/{orderId}/items | /orderItems?orderId=... |
[Standard] 子资源用于表达”从属”关系,且子资源的主键由父资源限定。
[Default] 多对多或无强从属关系的关联,使用查询参数过滤而非嵌套路径。
# 从属关系:订单属于某个用户GET /users/123/orders # 获取用户 123 的订单POST /users/123/orders # 为用户 123 创建订单
# 非从属:标签与文章多对多,用过滤GET /articles?tag=ai # 获取标签为 ai 的文章# 反模式 1:动词 + 名词(RPC 风格)GET /getUsersPOST /createUserPOST /deleteUser?id=123
# 反模式 2:层级过深GET /companies/1/departments/2/teams/3/members/4 # 4 级,应改为 /members?teamId=3
# 反模式 3:大小写混用或驼峰GET /userOrders/MyFirstOrder # 应使用 kebab-case:/user-orders/my-first-order[Requirement] 严格遵守 HTTP 方法语义,禁止用 POST 替代所有操作。
| 方法 | 语义 | 幂等 | 安全 | 典型用途 |
|---|---|---|---|---|
| GET | 读取资源 | 是 | 是 | 查询列表/详情 |
| POST | 创建资源 | 否 | 否 | 新建 |
| PUT | 全量替换资源 | 是 | 否 | 整体更新 |
| PATCH | 部分更新资源 | 否* | 否 | 局部字段更新 |
| DELETE | 删除资源 | 是 | 否 | 删除 |
*
PATCH本身不保证幂等,但业务实现应尽量幂等(见幂等性规范)。
[Standard] 成功响应使用恰当的状态码,禁止一律返回 200 OK。
| 场景 | 状态码 | 说明 |
|---|---|---|
| GET 成功 | 200 | 返回资源 |
| POST 创建成功 | 201 | 响应 Location 头指向新资源 |
| PUT/PATCH 成功 | 200 | 返回更新后的资源 |
| DELETE 成功 | 204 | 无响应体 |
| 异步任务已接受 | 202 | 任务入队,返回任务 ID |
| 参数错误 | 400 | 校验失败 |
| 未认证 | 401 | 缺少或无效 Token |
| 无权限 | 403 | 已认证但无权操作 |
| 资源不存在 | 404 | URI 或目标资源不存在 |
| 冲突 | 409 | 并发冲突/唯一性冲突 |
| 服务端错误 | 500 | 未预期异常 |
| 服务不可用 | 503 | 过载或维护中 |
# 创建用户
POST /v1/users HTTP/1.1
Content-Type: application/json
{"name":"张三","email":"zhang@example.com"}
HTTP/1.1 201 Created
Location: /v1/users/1001[Requirement] URI 结构为:/{api-prefix}/{version}/{resource}/{id},其中 api-prefix 统一为 /api。
[Standard] 路径参数使用资源名的 camelCase 形式:/users/{userId}、/orders/{orderId}。
[Default] API 版本放在路径中(见《API版本管理规范》),不使用域名区分版本。
https://api.example.com/api/v1/users/1001/orders?status=paid&page=2&page_size=20└───── 域名 ─────┘ └prefix┘ └v┘ └资源┘ └id┘ └子资源┘ └──── 查询参数 ────┘[Standard] 查询参数使用 snake_case(与 Python/Go 后端约定一致),布尔参数显式传 true/false。
| 参数类型 | 示例 |
|---|---|
| 过滤 | ?status=paid&type=vip |
| 时间范围 | ?created_from=2026-01-01T00:00:00Z&created_to=2026-01-31T23:59:59Z |
| 排序 | ?sort=-created_at,price(- 表示降序) |
| 字段选择 | ?fields=id,name,email |
| 分页 | ?page=1&page_size=20 或 ?cursor=eyJ... |
# 反模式:操作放入 URIGET /api/v1/users/123/activate # 应用 PATCH /users/123 {active:true}POST /api/v1/orders/456/cancel # 应用 POST /orders/456/cancel 或 PATCHDELETE /api/v1/users/123/avatar # 应用 DELETE /users/123/avatar (子资源 OK)注:资源上的非 CRUD 动作(如”激活""取消”),当无法用资源状态表达时,可使用动名词子路径:
POST /orders/{id}/cancel,但需在文档中明确其幂等性。
[Standard] 列表接口必须支持分页,禁止一次返回全量数据。
| 策略 | 适用场景 | 参数 |
|---|---|---|
| 偏移分页(Offset) | 管理后台、可跳页 | page、page_size |
| 游标分页(Cursor) | 信息流、大表、实时数据 | cursor、page_size |
| seek 分页 | 对性能要求极高的大表 | last_id、page_size |
[Default] 默认偏移分页,参数为 page(从 1 开始)和 page_size(默认 20,最大 100)。
[Requirement] 响应体必须返回分页元信息:total(总数)、page、page_size、total_pages。
{
"code": 0,
"message": "ok",
"data": {
"items": [ { "id": 1, "name": "张三" } ],
"pagination": {
"page": 1,
"page_size": 20,
"total": 156,
"total_pages": 8
}
}
}[Standard] 大表或频繁写入的列表使用游标分页,响应返回 next_cursor,无更多数据时返回 null。
{
"data": {
"items": [...],
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true
}
}# 反模式 1:不分页,返回全量GET /users # 10 万用户一次性返回# 反模式 2:page_size 无上限GET /users?page_size=99999 # 应限制最大 100,超大请求返回 400# 反模式 3:深分页未限制GET /users?page=100000&page_size=20 # 大偏移导致慢查询,应改用游标分页[Standard] 排序参数名为 sort,多字段用逗号分隔,字段前加 - 表示降序,不加表示升序。
[Requirement] 仅允许在文档中声明的可排序字段上排序,未声明字段返回 400。
GET /api/v1/orders?sort=-created_at,total_amount
# 含义:按创建时间降序,同时间按金额升序[Standard] 简单等值过滤直接用 字段=值;范围过滤使用 字段_from / 字段_to 后缀;模糊匹配使用 字段_like。
| 操作 | 参数示例 | 说明 |
|---|---|---|
| 等值 | ?status=paid | 精确匹配 |
| 多值 | ?status=paid,pending | IN 查询 |
| 范围 | ?amount_from=100&amount_to=500 | 闭区间 |
| 模糊 | ?name_like=张 | LIKE %张% |
| 布尔 | ?active=true | 显式 true/false |
[Requirement] 所有过滤字段必须在白名单内,禁止将原始参数透传到 SQL(防止 SQL 注入)。
[Caution] 模糊查询字段必须有索引,且限制最小长度(如 name_like 不少于 2 个字符)。
# 反模式:未校验字段直接拼 SQLGET /users?sort=secret_field # 应返回 400 Invalid sort fieldGET /users?name_like=% # 空模糊匹配导致全表扫描