资源建模与 HTTP 契约:从 URL 到一致的错误和查询
0. 元信息
- 主题路径:
docs/topics/rest-api-design/subtopics/resource-http-contracts/README.md - 父主题:
rest-api-design - 主分类:Web 与后端
- 辅助分类:工程技术
- 适合对象:会写 HTTP handler、SQL 与自动化测试的后端开发者
- 建议周期:1~2 周(每周 6~8 小时)
- 前置知识:
http-1-2-3、db-and-sql - 最终目标:建立资源、URL、method、状态码、problem+json、分页与过滤的稳定契约
1. 学习路线
资源边界 → URL → method → status → problem+json → cursor/filter/sort
2. 阶段周数分配
精简子主题不固定周数。按 §3 顺序完成,卡住时回到 RFC、契约样例和可重复请求。
3. 九阶段表
| 阶段 | 核心知识 | 实践产出 | 学会标准 |
|---|---|---|---|
| 1 | 术语与边界 | 术语表 | 不混淆相邻概念 |
| 2 | HTTP/标准语义 | 请求矩阵 | 能引用规范解释 |
| 3 | 数据与状态 | schema/状态图 | 正常与冲突可区分 |
| 4 | 安全边界 | 威胁清单 | 非法路径被拒绝 |
| 5 | 失败语义 | 错误目录 | 客户端能稳定处理 |
| 6 | 兼容策略 | 变更表 | 能识别 breaking change |
| 7 | 工具验证 | curl/collection | 可重复运行 |
| 8 | 自动检查 | 契约测试 | 修改后会报警 |
| 9 | 小项目 | 可运行交付 | 第三方可复现 |
4. 第一周任务
精简版省略固定日程。做一个最小正常请求,再补认证失败、非法输入、冲突、重试与超时。
5. 阶段通用验收
精简版省略;每个产出至少保留命令、预期、实际与版本。
6. 最终验收
精简版省略;以 §3 第 9 阶段和 §9.4 问题口述检查为准。
7. 综合项目
精简版省略;成果并入父主题电商商品 / 订单 API。
本主题贡献
- 在父主题
rest-api-design的综合项目「电商商品 / 订单 API」中,本主题(资源建模与 HTTP 契约)负责 资源、URL、method、状态码、problem+json、cursor 分页 的稳定契约:把 §1 路线「资源边界 → URL → method → status → problem+json → cursor/filter/sort」落到/products、/products/{productId}、/orders、/orders/{orderId}的 OpenAPI 3.1 schema 上。 - 工程动作:用 OpenAPI 3.1 编写 schema(含 ETag、If-Match、429 / 409 / 412 / 422 / 410 等响应);用
application/problem+json(RFC 7807 / 9457)做统一错误体(type / title / status / detail / instance / code / errors[].pointer);用cursor+limit做稳定分页;提供 curl 样例从「正常路径」走到 401 / 403 / 404 / 409 / 412 / 422 / 429 / 5xx 全套失败路径;交付 1 套可重复运行的 curl collection 或 Bruno / Postman 文件。 - 与 rest-api-design 其它子主题对接:把契约作为 performance-delivery 的 Idempotency-Key / ETag 落点;作为 security-governance 的
securitySchemes/scope入口;为 openapi-operations 提供 lint 与 diff 起点。
交付物清单:
openapi/resources.yaml:OpenAPI 3.1 spec,覆盖/products、/products/{productId}、/orders、/orders/{orderId}≥ 6 个 operation,包含components.securitySchemes引用与webhooks段;openapi/problem.yaml+errors/problem.json:≥ 8 种错误码 +application/problem+json响应模板(含errors[].pointer),并落errors/401.json、errors/404.json、errors/409.json等样例;collection/products.curl+collection/orders.bruno:≥ 20 个可重跑用例,含正常 + 401 / 403 / 404 / 409 / 412 / 422 / 429 / 5xx,make test一键重跑;spec/contract_check.md:契约测试(Schemathesis / Dredd / Prism mock)逐 operation 跑通,0 unexpected;附X-RateLimit-Remaining、ETag、Vary等 header 断言。
验收标准:
- 全部 20+ curl / Bruno 用例可
make test一键重跑,含断言 ETag / headers / status; - 状态码对照表覆盖 200 / 304 / 400 / 401 / 403 / 404 / 409 / 410 / 412 / 422 / 429 / 5xx 全 ≥ 12 种;
- 契约测试对每个 operation 抽样 ≥ 50 例(
--hypothesis-max-examples=50),无 unexpected; application/problem+jsonbody 必含type / title / status / detail / instance / code / errors[]且code为稳定字符串。
8. 推荐资料
精简版省略;使用 §9.3 和 §9.6 Source。
9. 学习资料汇聚(v0.3 自包含)
9.1 背景与动机
建立资源、URL、method、状态码、problem+json、分页与过滤的稳定契约。API 一旦被客户端依赖,错误的语义会变成多年兼容成本。我们用规范、可运行样例和失败测试约束选择。
9.2 概念地图
flowchart LR
Design[设计] --> Contract[HTTP/OpenAPI 契约]
Contract --> Runtime[实现/网关]
Runtime --> Client[客户端]
Runtime --> Evidence[日志/指标/trace]
Test[契约与失败测试] -.验证.-> Contract
Test -.验证.-> Runtime
这张图把设计、实现、调用和证据串起来。任何规则都要能落到契约或运行时检查。
9.3 基础知识讲解
9.3.1 论文 / 规范
- Roy Fielding, Architectural Styles and the Design of Network-Based Software Architectures:REST 起源。
- RFC 9110, HTTP Semantics:方法、状态码、缓存语义。
- RFC 7807, Problem Details for HTTP APIs:统一错误对象。
9.3.2 书
- Leonard Richardson, Mike Amundsen, Sam Ruby, RESTful Web APIs。
- Arnaud Lauret, The Design of Web APIs。
- JJ Geewax, API Design Patterns。
9.3.3 博客 / 文档
- Stripe API 文档:idempotency、分页、错误风格。
- Microsoft REST API Gui:兼容与一致性。
- Google API Design Guide:资源导向设计。
- Apigee 文档 与 AWS API Gateway 文档:网关、配额、部署。
9.3.4 人物
- Roy Fielding:REST 与 HTTP 架构。
- Tim Berners-Lee:Web、URI、HTTP。
- Darrel Miller:OpenAPI Initiative 技术治理与工具生态。
9.3.5 方法
- Contract-first:先写 OpenAPI 和示例,再写 handler。
- Consumer-driven review:从调用方任务检查命名、错误和兼容。
- Single-variable test:一次只改变 method、header、身份或并发量。
- Compatibility budget:发布前列出 breaking change 与迁移期。
9.4 经典问题与经典案例
| 问题 | 为什么重要 | 最简答案 |
|---|---|---|
| 资源名词化还是动作化 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
| POST/PUT/PATCH 如何选 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
| 401/403 如何分 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
| 409/412/422 如何分 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
| cursor 如何防重漏 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
| 批量操作如何报告部分失败 | 影响客户端正确性和长期兼容 | 写清语义、失败码、重试与测试证据 |
9.4.1 实战片段:状态码对照、OpenAPI、problem+json
| 场景 | 状态码 | 含义 | 客户端常见处理 |
|---|---|---|---|
| 资源不存在,且未确认过 | 404 Not Found | 路径或 id 没命中 | 不重试;按业务决定是否创建 |
| 资源曾经存在但已删除 | 410 Gone | 永久删除 | 提示用户;从列表里移除 |
| 字段语义校验失败 | 422 Unprocessable Entity | 语法 OK、语义失败 | 展示错误,修正后重提交 |
| 状态机/唯一约束冲突 | 409 Conflict | 当前状态不接受此操作 | 拉最新状态再决定 |
| 并发更新 ETag 不匹配 | 412 Precondition Failed | 资源已被改动 | 重新拉取、合并、二次提交 |
| 限流 | 429 Too Many Requests | 配额或速率触发 | 退避 + Retry-After |
openapi: 3.1.0
info:
title: Shop API
version: 1.0.0
description: 商品与订单资源
servers:
- url: https://api.example.com
paths:
/products:
get:
summary: 列出商品
parameters:
- {name: cursor, in: query, schema: {type: string}}
- {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
- {name: status, in: query, schema: {type: string, enum: [active, archived]}}
responses:
'200':
description: 商品列表
headers:
X-RateLimit-Remaining: {schema: {type: integer}}
content:
application/json:
schema:
type: object
required: [data, page]
properties:
data:
type: array
items: {$ref: '#/components/schemas/Product'}
page:
type: object
properties:
nextCursor: {type: [string, "null"]}
examples:
ok:
value:
data:
- {id: p_1, name: 茶杯, price: 1200, currency: CNY, etag: W/"abc"}
page: {nextCursor: eyJpZCI6InBfMiJ9}
/products/{productId}:
parameters:
- {name: productId, in: path, required: true, schema: {type: string}}
get:
responses:
'200':
description: 商品详情
headers:
ETag: {schema: {type: string}}
content:
application/json:
schema: {$ref: '#/components/schemas/Product'}
'304': {description: 未变化}
'404':
description: 资源不存在
content:
application/problem+json:
schema: {$ref: '#/components/schemas/Problem'}
'410':
description: 已删除
content:
application/problem+json:
schema: {$ref: '#/components/schemas/Problem'}
put:
parameters:
- {name: If-Match, in: header, required: true, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ProductUpsert'}
responses:
'200':
description: 替换成功
headers: {ETag: {schema: {type: string}}}
'409':
description: 唯一约束冲突
content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
'412': {description: ETag 不匹配(并发冲突)}
'422':
description: 字段语义错误
content:
application/problem+json:
schema: {$ref: '#/components/schemas/Problem'}
components:
schemas:
Product:
type: object
required: [id, name, price, currency]
properties:
id: {type: string}
name: {type: string, minLength: 1, maxLength: 200}
price: {type: integer, minimum: 0, description: 最小货币单位}
currency: {type: string, pattern: '^[A-Z]{3}$'}
etag: {type: string}
ProductUpsert:
type: object
required: [name, price, currency]
properties:
name: {type: string, minLength: 1, maxLength: 200}
price: {type: integer, minimum: 0}
currency: {type: string, pattern: '^[A-Z]{3}$'}
Problem:
type: object
required: [type, title, status]
properties:
type: {type: string, format: uri}
title: {type: string}
status: {type: integer}
detail: {type: string}
instance: {type: string}
code: {type: string, description: 稳定业务错误码}
errors:
type: array
items:
type: object
properties:
pointer: {type: string}
code: {type: string}
message: {type: string}
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "请求体字段不通过校验",
"instance": "/products/p_1",
"code": "validation_failed",
"errors": [
{"pointer": "/price", "code": "min_value", "message": "must be >= 0"},
{"pointer": "/currency", "code": "pattern", "message": "must match ^[A-Z]{3}$"}
]
}
# curl:条件 GET 与并发更新
curl -i https://api.example.com/products/p_1
curl -i -H 'If-None-Match: W/"abc"' https://api.example.com/products/p_1
curl -i -X PUT https://api.example.com/products/p_1 \
-H 'Content-Type: application/json' \
-H 'If-Match: W/"abc"' \
-d '{"name":"茶杯","price":1300,"currency":"CNY"}'
# 幂等创建:同 key 不重复扣款
curl -i -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 9f2b6c1a-1f3d-4b8e-9b6c-2a7f0b5c91a2' \
-d '{"productId":"p_1","quantity":2}'
9.5 学习难点
- 概念难点:规范术语相近。卡点来自用框架默认值代替 HTTP 语义;回 RFC 和 wire example。
- 思维难点:正常流很短,重试、并发、权限和演进才难;先列状态机与不变量。
- 工程难点:网关、应用、数据库和客户端行为不同;用端到端请求、契约测试和 trace 定位边界。
9.6 技术标准与接口
9.6.1 Entity
| 名称 | 版本 | 组织 | 状态 / 可访问性 |
|---|---|---|---|
| OpenAPI Specification | 3.1 | OpenAPI Initiative | 正式;Apache-2.0 仓库,规范公开 |
| JSON:API | 1.1 | JSON:API 社区 | 正式;公开 |
| JSON Schema | 2020-12 | JSON Schema / IETF 社区 | 正式;公开 |
| Problem Details for HTTP APIs | RFC 7807 | IETF | 已被 RFC 9457 更新;RFC 公开 |
| HAL | draft-kelly-json-hal | IETF Internet-Draft / HAL 社区 | 草案;公开 |
9.6.2 Scope
OpenAPI 描述 HTTP API;JSON Schema 描述数据约束;JSON:API 与 HAL 约定表示和链接;Problem Details 约定机器可读错误。它们不替代业务授权、事务设计或运行时保护。
9.6.3 Structure
OpenAPI 必会 paths、operations、parameters、requestBody、responses、components、securitySchemes;JSON Schema 必会 type、required、properties、组合与引用;Problem Details 必会 type/title/status/detail/instance。
9.6.4 Ecosystem
实现与工具包括 Swagger UI、Redoc、Spectral、OpenAPI Generator、Postman、Insomnia、Bruno、httpie、curl。API gateway 常用 Apigee、AWS API Gateway、Kong、Envoy。工具默认值不是规范。
9.6.5 Depth Tiers
| 层级 | 可观察能力 |
|---|---|
| L0 | 知道这些标准解决什么问题 |
| L1 | 看懂 schema、operation 和 problem body |
| L2 | 能正确编写、校验、生成客户端并调用 |
| L3 | 能解释语义、排错兼容/鉴权/缓存/限流问题 |
| L4 | 能设计扩展、治理规则与跨团队演进机制 |
本计划目标:L3。
9.6.6 Source
- OpenAPI Initiative 仓库:OpenAPI 3.1 原文与版本历史。
- IETF RFC Editor:RFC 9110、7807/9457、6585 等原文。
- JSON Schema 2020-12 与 JSON:API 1.1。
- 引用快照:2026-07-30。实施前检查更新、勘误与废止关系。
10. 常见误区
- 用框架默认值代替明确契约
- 只测 2xx
- 错误 body 无稳定 code
- 混淆认证与授权
- 重试无 idempotency
- 缓存 key 漏身份/查询条件
- 版本化代替兼容设计
- 文档与实现分开维护
- 日志泄露 secret
- 只有 dashboard,没有可定位的 trace。
11. 所有知识点分类(统一规则)
- 编程语言 2. 数据结构与算法 3. 计算机基础 4. 工程技术 5. Web 与后端 6. 前端与客户端 7. 数据与人工智能 8. 项目与职业能力 9. 安全与可靠性
本计划归属:Web 与后端 主 + 工程技术 辅。