适用对象:架构师、开发、测试、技术负责人及 API 治理人员|示例领域:订单管理
目录
- 目的与通用原则
- 技术选型
- [RESTful API 规范](#RESTful API 规范)
- [GraphQL API 规范](#GraphQL API 规范)
- [gRPC API 规范](#gRPC API 规范)
- 跨协议一致性
- 安全、治理与演进
- 反模式与评审清单
- 参考标准
1. 目的与通用原则
本规范用于指导公开 API、内部服务和微服务接口的设计、评审、实现与演进。API 是提供方与调用方长期依赖的契约,不是数据库表、控制器或内部函数的网络映射。
规范性用语:必须 表示强制要求;不得 表示禁止;应该 表示强烈推荐,偏离时需说明理由;建议 表示通常有益;可以表示可选方案。路径使用复数名词等属于组织约定,并非 HTTP 标准本身的强制要求。
通用原则如下:
- 接口必须围绕用户、商品、订单、支付、履约等稳定领域概念建模,不得暴露数据库、缓存或框架细节。
- REST 使用 OpenAPI、GraphQL 使用 SDL、gRPC 使用
.proto描述契约;契约、实现、示例和测试必须同步。 - 同一概念在不同协议中保持相同名称和语义。
orderId与order_id都表示公开订单标识,不能一处表示数据库主键、另一处表示业务单号。 - 时间点使用带时区的 RFC 3339/ISO 8601 表达或协议原生时间类型;金额使用最小货币单位整数或精度明确的十进制类型,不得使用二进制浮点数。
- 必须区分字段缺失、
null、空字符串、零和空数组;集合为空时返回空集合,不返回null。 - 认证解决"是谁",授权解决"能做什么"。授权必须覆盖租户、对象、动作和敏感字段。
- 可重试的写操作必须定义幂等策略;调用链必须设置超时或 deadline,并携带可关联的追踪标识。
- 客户端与服务端不会同时升级。所有变更必须评估协议兼容、代码兼容和业务语义兼容。
2. 技术选型
| 维度 | REST | GraphQL | gRPC |
|---|---|---|---|
| 抽象模型 | 资源与 HTTP 统一接口 | 类型图与声明式查询 | 服务与远程方法 |
| 常见编码 | JSON | JSON | Protobuf 二进制 |
| 主要优势 | HTTP 生态、缓存、开放性 | 客户端按需取数、关联查询 | 强类型、高效率、流式通信 |
| 主要风险 | RPC 风格 URL、状态码滥用 | N+1、查询成本、字段授权 | deadline 缺失、错误码粗糙、兼容破坏 |
| 典型场景 | 公共平台、资源操作 | 多端页面聚合 | 内部微服务、实时流 |
公共开放平台通常优先 REST;客户端需要灵活组合复杂关联数据时可以采用 GraphQL;内部高吞吐、强类型或双向流通信可以采用 gRPC。一个系统可以对外提供 REST/GraphQL、对内使用 gRPC,但网关必须明确字段、身份、超时与错误映射。大文件传输应综合对象存储、CDN 和断点续传,不应仅因 gRPC 支持流而选用它。
3. RESTful API 规范
3.1 资源与 URL
URL 表达资源身份,HTTP 方法表达操作语义。本规范约定路径段使用小写 kebab-case,集合采用复数名词,不带文件扩展名,不以尾斜杠区分资源。
text
# 正确
GET /v1/orders
POST /v1/orders
GET /v1/orders/{orderId}
PATCH /v1/orders/{orderId}
GET /v1/orders/{orderId}/items
# 错误
POST /v1/getOrders
GET /v1/order/list.json
POST /v1/mysqlOrderRecords/query
嵌套路径只表达稳定归属,可独立定位的子资源应提供顶级地址。过滤、排序和分页使用查询参数。URL 不得包含密码、令牌、身份证号等敏感数据。
业务动作优先建模为状态更新或新资源。订单导出会产生有生命周期的任务,应使用 POST /v1/order-export-jobs。确实无法自然表达的动作可以统一为 POST /v1/orders/{id}:cancel,但必须说明前置条件和幂等性,不得扩散出含义模糊的 :process、:handle。
3.2 HTTP 方法
"安全"表示客户端没有请求改变资源状态,不代表服务器不能记录日志。"幂等"表示重复同一请求的预期效果与执行一次相同,不要求每次响应完全一致。
| 方法 | 安全 | 幂等 | 用途 |
|---|---|---|---|
| GET/HEAD | 是 | 是 | 获取表示或元数据 |
| POST | 否 | 否,可增加幂等机制 | 创建、提交动作、启动任务 |
| PUT | 否 | 是 | 已知 URI 上创建或完整替换 |
| PATCH | 否 | 不保证 | 部分更新 |
| DELETE | 否 | 是 | 请求删除资源 |
| OPTIONS | 是 | 是 | 通信选项和跨域预检 |
GET 请求体没有通用的可互操作语义,不得依赖它传递查询。HEAD 响应不包含内容。PATCH 必须声明补丁媒体类型,如 application/merge-patch+json。DELETE 不承诺底层数据立即物理擦除。
创建订单示例:
http
POST /v1/orders HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: 6cc82b57-0a7b-45ad-b28d-0bf8688d5b31
{"customerId":"cus_10001","items":[{"productId":"prd_20001","quantity":2}],"currency":"CNY"}
http
HTTP/1.1 201 Created
Location: /v1/orders/ord_202608140001
ETag: "order-7"
Content-Type: application/json
{"id":"ord_202608140001","status":"PENDING_PAYMENT","amountMinor":19900,"currency":"CNY"}
相同调用方使用相同幂等键和请求内容时,应复用第一次结果;相同键对应不同内容时返回 409 Conflict。幂等键应与调用方、操作和请求指纹绑定,并设置保留期限。
3.3 状态码
| 状态码 | 使用规则 |
|---|---|
| 200 | 成功且返回表示 |
| 201 | 资源已创建;适用时返回 Location |
| 202 | 请求已接受但处理未完成;提供任务查询方式 |
| 204 | 成功且无内容;不得携带响应体 |
| 206 | 仅用于 Range 部分表示,不表示业务部分成功 |
| 304 | 条件 GET/HEAD 未修改;无响应体 |
| 400 | 报文语法或基本结构无效 |
| 401 | 缺少有效认证凭据;适用时返回 WWW-Authenticate |
| 403 | 身份已知但无权操作 |
| 404/410 | 不存在或明确永久移除 |
| 405 | 方法不允许;返回 Allow |
| 409 | 与资源当前业务状态冲突 |
| 412 | If-Match 等前置条件失败 |
| 415 | 请求媒体类型不支持 |
| 422 | 可解析但内容不符合语义校验 |
| 428 | 服务端要求条件请求 |
| 429 | 限流或配额耗尽;适用时返回 Retry-After |
| 500 | 本服务内部未知故障 |
| 502/504 | 网关收到无效上游响应或等待上游超时 |
| 503 | 服务暂不可用;可返回 Retry-After |
不得把所有错误都返回 200。400 处理报文或基本请求无效,422 处理内容语义校验,409 处理业务状态冲突,412 专用于显式 HTTP 前置条件失败。401 关注认证,403 关注授权。
3.4 请求、响应与错误
Content-Type 描述实际内容,Accept 描述期望表示,Authorization 承载凭据。不得在业务 JSON 中重复传递令牌、角色或可从可信上下文获取的租户身份。成功响应不强制包装为 code/message/data,避免与 HTTP 语义重复;不得直接序列化 ORM 实体。
错误应该采用 RFC 9457 application/problem+json:
json
{
"type":"https://api.example.com/problems/validation-error",
"title":"请求字段校验失败",
"status":422,
"detail":"一个或多个字段不符合要求。",
"instance":"/problems/01J5A9Z7Q79JGN9X5K2A",
"code":"VALIDATION_FAILED",
"traceId":"4bf92f3577b34da6a3ce929d0e0e4736",
"errors":[{"pointer":"/items/0/quantity","code":"OUT_OF_RANGE","detail":"必须介于1和99之间"}]
}
程序依赖 type 或稳定的 code,不得解析本地化 detail。对外错误不得包含堆栈、SQL、文件路径、密钥或内部主机名;详细诊断通过 traceId 关联受保护日志。
3.5 分页、并发、缓存和版本
页码分页适合低变化、需要跳页的管理界面;高频变化或大数据集合使用游标分页。查询必须有稳定排序并以唯一键决胜。游标必须不透明、不可伪造,并绑定租户、筛选和排序条件。页面大小必须有默认值和上限,昂贵场景不得强制返回总数。
text
GET /v1/orders?status=PAID&sort=-createdAt,id&limit=20&after=eyJ2IjoxLCJrIjoiLi4uIn0
并发更新建议使用 ETag 与 If-Match;条件不成立返回 412,服务端要求条件更新时可对无前置条件的请求返回 428。自动重试必须有幂等保证,采用有上限的指数退避与抖动,并尊重 Retry-After。
GET 不表示一定可缓存。服务端通过 Cache-Control 明确策略,使用 ETag/Last-Modified 验证,并正确设置 Vary。私有或租户数据不得被共享缓存错误复用。
公开 REST API 可使用路径主版本 /v1。新增可选字段通常兼容;删除或重命名字段、改变类型、必填性、校验、默认排序及同步/异步语义都可能破坏兼容。弃用必须提供替代方案、使用监控、迁移窗口和下线日期。
3.6 批量操作与异步任务
批量接口必须规定单批数量、请求体大小、超时、原子性以及部分成功的表达方式。每个请求项都应携带稳定的客户端关联标识,使调用方能够把结果对应到原始输入。如果允许部分成功,响应必须逐项给出成功资源或稳定错误;如果要求全有或全无,文档必须说明事务边界。不得用一个模糊的整体 200 掩盖部分条目失败。单项重试前还要确认其幂等性,避免已经成功的条目被重复执行。
耗时导出、批量计算等操作应建模为任务资源,而不是让连接无限等待:
http
POST /v1/order-export-jobs HTTP/1.1
Content-Type: application/json
{"filter":{"status":"PAID"},"format":"CSV"}
http
HTTP/1.1 202 Accepted
Location: /v1/order-export-jobs/job_90001
Retry-After: 5
{"id":"job_90001","status":"QUEUED","createdAt":"2026-08-14T08:40:00Z"}
任务至少区分 QUEUED、RUNNING、SUCCEEDED、FAILED、CANCELLED,并定义进度、取消、失败原因、结果保留期和重复提交规则。结果文件建议放入对象存储并返回短期签名地址。轮询必须有建议间隔;也可以使用 Webhook、事件或订阅通知,但回调必须验证来源、支持重复投递并避免携带敏感结果。
3.7 请求头与数据约定
Accept-Language 可协商人类可读文本,但机器逻辑不得依赖本地化文本。追踪优先使用组织采用的标准上下文,例如 W3C Trace Context;业务请求 ID、追踪 ID 和幂等键用途不同,不得混用。服务端不得信任客户端自行声明的用户、角色或租户,必须从已验证凭据和可信网关上下文取得。
标识符在 JSON 中应统一为字符串,避免大整数精度差异。布尔字段应采用能够读出真假含义的名称。枚举值一旦公开不得改变原义,并必须规定客户端遇到未知值时的回退行为。日期区间要说明端点是否包含,数量要说明单位和范围,货币必须同时包含金额和货币代码。
4. GraphQL API 规范
GraphQL Schema 是面向客户端能力的类型契约,不得机械映射数据库。类型、接口和输入对象使用 PascalCase,字段和参数使用 camelCase,枚举值使用 UPPER_SNAKE_CASE。Query 用于读取,Mutation 用于副作用,Subscription 用于持续更新;其网络传输需另行选定。
graphql
scalar DateTime
scalar Long
enum OrderStatus { ORDER_STATUS_UNSPECIFIED PENDING_PAYMENT PAID COMPLETED CANCELLED }
type Money { amountMinor: Long! currency: String! }
type Order { id: ID! status: OrderStatus! total: Money! deliveryNote: String createdAt: DateTime! }
type PageInfo { hasNextPage: Boolean! endCursor: String }
type OrderConnection { nodes: [Order!]! pageInfo: PageInfo! }
input CreateOrderItemInput { productId: ID! quantity: Int! }
input CreateOrderInput { customerId: ID! items: [CreateOrderItemInput!]! currency: String! }
interface BusinessError { code: String! message: String! }
type OrderStateError implements BusinessError { code: String! message: String! currentStatus: OrderStatus! }
type CreateOrderPayload { order: Order errors: [BusinessError!]! }
type Query { order(id: ID!): Order orders(first: Int = 20, after: String): OrderConnection! }
type Mutation { createOrder(input: CreateOrderInput!): CreateOrderPayload! cancelOrder(id: ID!): CreateOrderPayload! }
查询与变量示例:
graphql
query ListOrders($first: Int!, $after: String) {
orders(first: $first, after: $after) {
nodes { id status total { amountMinor currency } createdAt }
pageInfo { hasNextPage endCursor }
}
}
json
{"first":20,"after":"eyJ2IjoxLCJrIjoiLi4uIn0"}
GraphQL 默认可空,! 表示非空。非空字段解析失败会向最近的可空父字段传播;不得照搬数据库 NOT NULL,应结合旧数据、权限隐藏和下游故障确定。输入字段缺失与显式 null 可能不同,部分更新必须区分"不修改"和"清空"。
列表优先使用不透明游标和 Connection/PageInfo 模型。服务端必须联合限制查询深度、字段成本、列表基数、别名、嵌套乘积、响应大小和执行时间,只限制深度不足以防止昂贵查询。
Mutation 应返回变更后的资源及类型化业务错误,不应只返回 true。库存不足、状态不允许等可预期结果可放入 Payload;语法、验证、Resolver 异常和系统故障进入顶层 errors。顶层 Mutation 字段串行执行,不代表分布式事务自动原子。
类型化业务拒绝示例:
json
{
"data": {
"cancelOrder": {
"order": null,
"errors": [{
"code": "ORDER_STATE_CONFLICT",
"message": "已完成订单不能取消",
"currentStatus": "COMPLETED"
}]
}
}
}
GraphQL 响应可以同时包含部分 data 和 errors。客户端必须同时检查两者;程序使用 extensions.code,不得解析 message。不得规定 GraphQL 永远返回 HTTP 200:认证中间件、报文无法解析、媒体类型不支持或网关故障应使用适当 4xx/5xx。GraphQL over HTTP 当前内容若仍为草案,必须标注版本和状态,不得使用未注册状态码表示部分成功。
字段执行失败但其他字段成功时可以返回部分数据:
json
{
"data": {"order":{"id":"ord_202608140001","status":"PAID","deliveryNote":null}},
"errors": [{
"message":"无权读取配送备注",
"path":["order","deliveryNote"],
"extensions":{"code":"FIELD_ACCESS_DENIED","traceId":"4bf92f3577b34da6a3ce929d0e0e4736"}
}]
}
请求错误发生在执行前,例如文档语法、验证、操作选择或变量转换失败;字段错误发生在执行期间,并受 nullability 传播规则影响;可预期业务拒绝是领域结果。三者必须分开建模和监控。message 面向人类,错误分类使用稳定代码,不得暴露堆栈、SQL、查询计划或内部地址。
授权必须落实到领域对象和字段层,只保护顶层 Query 会被嵌套路径绕过。关闭内省不能替代授权。N+1 应通过请求作用域批量加载、数据源批量接口或查询规划解决,缓存不得跨用户或租户污染。
服务必须为深度、复杂度、别名数量、列表长度、响应大小和执行时间设置上限,并把嵌套列表乘积计入成本。受控客户端可以采用持久化查询、允许列表和查询签名。Subscription 必须规定连接认证、身份续期、空闲超时、断线恢复、事件顺序和权限变化后的处理;长期连接不能沿用已经失效的权限。文件上传建议使用对象存储直传,不要默认采用缺乏统一标准约束的上传扩展。
DataLoader 只解决同一请求范围内可批量合并的数据访问,不会自动解决低效 SQL、下游超时或无限列表。监控应覆盖操作名称、关键字段延迟、下游调用数和估算成本,不能把任意查询全文作为指标标签。
删除字段前使用 @deprecated 给出替代方案并分析实际操作使用量。改变类型、参数、nullability 或为已有字段增加必填参数属于破坏性变化;新增枚举值也可能破坏客户端穷举逻辑。
多团队只有在确有独立所有权和发布边界时才应引入 Federation 或组合 Schema,并明确实体所有者、跨子图依赖和可用性预算。Schema Registry 应在合并和发布前执行破坏性变更检测。字段弃用后要监控已注册操作,确认调用清零再删除。
5. gRPC API 规范
服务应围绕稳定业务能力设计,使用 OrderService、GetOrder、ListOrders、CreateOrder 等清晰名称。字段使用 lower_snake_case,消息使用 PascalCase,枚举值使用大写下划线;首个枚举值必须为数值 0 的 *_UNSPECIFIED。
protobuf
syntax = "proto3";
package example.orders.v1;
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc UpdateOrder(UpdateOrderRequest) returns (Order);
rpc WatchOrder(WatchOrderRequest) returns (stream Order);
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING_PAYMENT = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_COMPLETED = 3;
ORDER_STATUS_CANCELLED = 4;
}
message Money { int64 amount_minor = 1; string currency = 2; }
message Order {
reserved 8;
reserved "legacy_number";
string id = 1;
OrderStatus status = 2;
Money total = 3;
optional string delivery_note = 4;
google.protobuf.Timestamp created_at = 5;
string etag = 6;
}
message GetOrderRequest { string id = 1; }
message ListOrdersRequest { int32 page_size = 1; string page_token = 2; string filter = 3; string order_by = 4; }
message ListOrdersResponse { repeated Order orders = 1; string next_page_token = 2; }
message CreateOrderRequest { string customer_id = 1; string currency = 2; string request_id = 3; }
message UpdateOrderRequest { Order order = 1; google.protobuf.FieldMask update_mask = 2; }
message WatchOrderRequest { string id = 1; }
字段编号一旦发布不得修改或复用,删除字段必须 reserved 编号,建议同时保留名称。不得为"整理顺序"重新编号。新增字段在二进制层通常兼容,但仍需评估源代码、ProtoJSON 和业务语义。API 消息应与存储消息分离。
Protobuf 标量具有默认值,默认值不一定表示调用方显式传入。需要区分"未设置"和"设置为默认值"时,应使用支持存在性的 optional、消息字段或受控的 oneof。oneof 只用于真正互斥的选择。不要用当前只有两种状态的布尔值表示未来可能增加状态的业务概念。Any 会降低类型可发现性和兼容检查能力,只有具备类型注册、白名单和生命周期治理时才可采用。
应该优先使用 Timestamp、Duration、FieldMask 等 Well-Known Types,而不是自定义含义不明的字符串。FieldMask 的路径、可更新字段和清空语义必须写入文档。大块二进制数据要限制单消息大小,并评估对象存储或受控流式传输;不得让调用方用无限消息绕过总量限制。
gRPC 支持 Unary、服务端流、客户端流和双向流。普通请求默认使用 Unary;流式调用必须定义消息边界、顺序、背压、心跳、空闲超时、结束、取消和断线恢复。单个流内有序不表示多个 RPC 全局有序,取消也不会回滚已发生的副作用。
| RPC 类型 | 调用形态 | 适用场景 | 必须考虑的问题 |
|---|---|---|---|
| Unary | 单请求、单响应 | 常规查询、创建、更新 | deadline、幂等和重试 |
| Server Streaming | 单请求、响应流 | 状态观察、连续结果 | 背压、断点和长连接资源 |
| Client Streaming | 请求流、单响应 | 分块采集、客户端汇总上传 | 部分接收、大小、取消 |
| Bidirectional Streaming | 双向独立消息流 | 实时协作、交互控制 | 状态机、重连、顺序和权限变化 |
如果分页 Unary 已经满足需求,通常不应改为服务端流。流式调用失败时,已经收到的消息不会因最终状态失败而自动失效;调用方必须知道结果是可增量提交、全部成功后提交,还是需要补偿。断线恢复方案应定义续传令牌或最后确认序号,服务端必须能够识别重复消息。
每个客户端 RPC 必须设置 deadline,并向下游传播剩余预算。客户端超时不代表服务端未完成写入;只有具备幂等保证的调用可以自动重试。重试必须限制次数、总时长并采用指数退避与抖动,避免多层重试风暴。
deadline 是绝对截止点,timeout 是从当前时间计算的持续时间。跨主机传播时应使用框架支持的 deadline/timeout 机制,避免直接传播未校正的本地时钟值。下游预算应小于上游剩余时间,为序列化、网络返回和清理保留空间。服务端要定期检查上下文是否取消,并停止无意义的数据库扫描或下游调用。
透明重试、客户端策略重试和业务补偿不是同一个概念。写操作即使返回 DEADLINE_EXCEEDED,也可能已经完成,因此创建订单等方法必须通过 request_id 去重并支持结果查询。熔断、负载保护和重试预算要联合设计;系统过载时继续重试会放大故障。
状态码应精确:参数本身无效用 INVALID_ARGUMENT;资源不存在用 NOT_FOUND;缺少身份用 UNAUTHENTICATED;无权限用 PERMISSION_DENIED;当前状态不允许操作用 FAILED_PRECONDITION;并发事务冲突用 ABORTED;配额耗尽用 RESOURCE_EXHAUSTED;临时不可用用 UNAVAILABLE;内部不变量失败用 INTERNAL。不得把所有异常转为 UNKNOWN。
| 状态 | 选择边界 |
|---|---|
ALREADY_EXISTS |
创建指定身份的实体,但实体已存在 |
OUT_OF_RANGE |
参数超出可迭代的有效范围,如读取位置越界 |
UNIMPLEMENTED |
服务未实现方法或能力,不表示临时不可用 |
CANCELLED |
调用被主动取消 |
DEADLINE_EXCEEDED |
调用未在截止时间完成 |
DATA_LOSS |
检测到不可恢复的数据损坏或丢失 |
UNKNOWN |
跨边界收到无法分类的错误,不应成为默认兜底 |
INVALID_ARGUMENT 与资源当前状态无关;"已完成订单不能取消"是 FAILED_PRECONDITION。ABORTED 常表示并发或事务冲突,调用方通常需要从更高层重新读取并重做操作。RESOURCE_EXHAUSTED 描述配额、速率或容量耗尽,UNAVAILABLE 描述服务暂时无法处理。是否重试还必须结合方法幂等性和结构化重试提示。
错误文本供人阅读,程序使用状态码和结构化详情。gRPC 状态与 HTTP 状态不是一一等价关系,网关必须按业务语义显式映射。认证、追踪等通过 metadata 传递;自定义 key 不得使用 grpc- 保留前缀,二进制 key 使用 -bin 后缀。
可以使用 google.rpc.Status 及 BadRequest、ErrorInfo、RetryInfo、ResourceInfo 等详情承载字段错误、稳定原因、重试建议和资源信息,但要确认各语言及代理链支持。客户端不得解析自然语言 message。详情不得包含堆栈、内部主机、数据库语句或秘密。
Metadata 只承载调用级元信息,不传输大型业务对象。必须区分服务身份、最终用户身份和委托身份,服务端对每个 RPC 执行授权。生产通道使用 TLS,高敏内部调用建议使用 mTLS 或等价工作负载身份。应限制最大消息、并发流、连接数和速率。反射便于调试,但不是安全措施,公网环境是否开放必须经过评估。
List 方法统一使用 page_size、page_token、next_page_token、filter 和 order_by。令牌必须不透明并绑定查询条件。长时间操作可以返回组织统一的 Operation/Job,明确名称、完成状态、进度、结果、错误、取消、轮询和保留期。批量方法要说明单项结果、原子性、最大批量数与重试边界。
package 包含主版本,如 example.orders.v1。滚动发布和回滚期间必须支持新旧版本共存;破坏性变化使用 v2 package、并行服务和迁移窗口。
改变字段编号、复用已删除 tag、把字段移动到已有 oneof 或改变相同字段的业务含义都可能破坏兼容。删除枚举值后要保留其编号和名称。即使官方线格式把某些类型变化列为兼容,也可能发生截断或信息损失;无法控制所有客户端升级顺序的公开接口不应依赖这种"条件兼容"。
6. 跨协议一致性
| 场景 | REST | GraphQL | gRPC |
|---|---|---|---|
| 创建订单 | POST /v1/orders,201 |
createOrder |
CreateOrder |
| 查询订单 | GET /v1/orders/{id} |
order(id:) |
GetOrder |
| 分页列表 | ?limit=&after= |
orders(first:,after:) |
ListOrders |
| 更新订单 | PATCH + If-Match |
专用 Mutation | UpdateOrder + FieldMask/etag |
| 状态冲突 | 409 | 类型化业务错误 | FAILED_PRECONDITION |
| 未认证/无权限 | 401/403 | HTTP 或字段错误 | UNAUTHENTICATED/PERMISSION_DENIED |
| 限流/暂不可用 | 429/503 | HTTP 或执行错误 | RESOURCE_EXHAUSTED/UNAVAILABLE |
该表是组织映射,不表示协议值天然等价。订单状态、金额单位、资源标识、租户边界和错误原因必须一致;REST traceparent、GraphQL extensions.traceId 和 gRPC tracing metadata 应关联同一条追踪。
跨协议转换必须保留原始语义。例如 gRPC FAILED_PRECONDITION 不能固定映射成一个 HTTP 状态:普通业务状态冲突通常映射 409,显式 If-Match 失败应映射 412,参数本身无效则更接近 400/422。GraphQL 中可预期业务拒绝可以放在类型化 Payload,但网关、认证和无法形成 GraphQL 响应的故障仍使用 HTTP 层错误。转换层应记录原协议状态和统一领域码,便于排障,而不是只保留转换后的结果。
跨协议字段应维护集中词汇表和状态机。任何协议新增订单状态、错误原因或必填约束时,都必须检查其他协议、数据存储、客户端生成代码和事件模型。相同领域码一经发布不得在另一接口中复用为不同含义。
7. 安全、治理与演进
- 所有生产流量必须使用 TLS,内部网络不得默认可信。OAuth 2.0/OIDC、API Key、mTLS 只解决各自范围的问题,不得替代对象和字段授权。
- 输入按允许列表校验,防范注入、SSRF、CSRF 和重放;文件上传限制类型、大小和数量,建议对象存储直传。
- 限制请求、响应、GraphQL 查询、gRPC 消息、连接、速率与配额。日志不得记录令牌、密码或完整敏感请求。
- 监控成功率、错误率、P95/P99 延迟、超时、取消、重试、限流、缓存命中、GraphQL 成本和 gRPC 流资源。
- 治理流程包括:领域设计评审、契约评审、lint、破坏性变更检查、安全评审、契约与集成测试、灰度发布、回滚验证、使用监控、弃用与下线。
- 契约、生成器和插件属于供应链,必须固定可信版本并审查升级差异。
7.1 身份与授权
OAuth 2.0 用于委托授权,OIDC 在其上提供身份层;API Key 通常标识调用应用,不能单独证明最终用户;mTLS 适合工作负载双向认证。选择机制时必须说明主体、凭据生命周期、轮换、撤销和泄露后的响应。权限模型应默认拒绝,以最小权限授予,并在租户、对象、动作和字段层验证。网关认证成功不代表下游可以跳过业务授权。
为了防止资源枚举,对无权查看的资源可以与不存在统一返回 404 或 NOT_FOUND,但该策略必须在同类接口保持一致。服务端不得直接信任请求中的 ownerId、role 或租户 ID。审计日志要记录主体、操作、资源、时间和结果,且不能被普通业务用户修改。
7.2 输入、数据与边界保护
所有输入都要校验类型、范围、长度、字符集、组合约束和权限。使用参数化数据库访问,禁止把输入直接拼接到 SQL、命令、模板或过滤表达式。服务端请求外部 URL 时要限制协议、目标域、解析后的 IP、重定向次数和私网地址,防止 SSRF。
浏览器使用 Cookie 认证时必须防范 CSRF;CORS 只约束浏览器跨源读取,不能替代认证和授权。上传文件应检查实际内容、大小和数量,隔离存储并进行恶意内容扫描。数据只收集业务所需最小集合,传输、日志、备份和分析环境遵循相同敏感级别与保留期限。
7.3 可观测性与发布治理
三种协议统一采集服务、操作、结果类别、延迟、请求/响应大小、重试和下游依赖。至少监控成功率、错误率、P50/P95/P99、超时、取消、限流、缓存命中、GraphQL 高成本字段以及 gRPC 流和消息大小。用户 ID、订单 ID、任意 GraphQL 文本不得直接作为无界指标标签。
发布前必须完成契约 lint、破坏性变更检测、安全评审、契约测试、集成测试、负载与故障测试。发布采用灰度和可回滚策略;上线后对错误、延迟和使用量设置告警。弃用流程包括声明替代方案、通知调用方、阻止新增依赖、监控旧接口调用、提供迁移窗口并验证清零后下线。不得在没有迁移计划时直接删除公开字段或方法。
8. 反模式与评审清单
典型反模式及修正:
| 反模式 | 修正 |
|---|---|
| URL 充斥动词、所有请求都 POST | 资源用名词,标准操作使用正确方法 |
| 所有响应都 200 | 使用正确 HTTP 状态,领域码补充细节 |
| 直接暴露数据库/ORM | 使用独立领域契约和输出白名单 |
| 分页无稳定排序 | 固定排序并以唯一键决胜 |
| 重试非幂等写 | 使用幂等键、请求指纹和有限重试 |
| GraphQL 只做顶层授权 | 在领域对象和字段数据层统一授权 |
| GraphQL 无成本限制 | 联合限制深度、复杂度、基数、大小与超时 |
| GraphQL 所有字段设为非空 | 按真实业务保证和错误传播范围设计 nullability |
GraphQL Mutation 只返回 true |
返回变更资源、关联 ID 和类型化业务错误 |
| GraphQL DataLoader 跨请求共享 | 将批量缓存限制在可信请求/租户边界内 |
| gRPC 不设置 deadline | 设置并传播调用预算 |
| 所有 gRPC 接口都使用双向流 | 默认 Unary,确有持续双向交互才使用流 |
| Proto 重新编号或复用 tag | 编号永久稳定,删除后 reserved |
| gRPC 错误全部 INTERNAL/UNKNOWN | 使用最具体状态和结构化详情 |
| 机械映射 HTTP 与 gRPC 状态 | 根据领域、前置条件和重试语义显式映射 |
| 日志记录令牌或完整请求体 | 按字段白名单记录并进行脱敏和访问控制 |
| 假设客户端与服务端同时升级 | 设计新旧共存、滚动发布和回滚窗口 |
评审时逐项填写"符合/不符合/不适用":
- 是否围绕稳定业务概念建模,且命名跨协议一致?
- 是否区分资源 ID、追踪 ID、幂等键和数据库主键?
- 金额、时间、枚举以及字段缺失和
null的语义是否明确? - REST URL、方法、状态码和 Problem Details 是否符合语义?
- 201 是否提供资源位置,202 是否提供任务查询,204/304 是否无响应体?
- 401 是否包含适当认证质询,405 是否提供
Allow? - 206 是否只用于 Range,而非普通业务部分成功?
- 分页是否有上限、稳定排序和不透明游标?
- 非幂等写是否定义幂等策略,并发更新是否有前置条件?
- 缓存是否考虑认证、租户、语言、编码和
Vary? - 批量接口是否说明原子性、逐项结果和重试边界?
- 异步任务是否说明状态、取消、结果和保留期限?
- GraphQL nullability、业务错误和部分数据是否明确?
- GraphQL 是否执行字段授权、成本限制和 N+1 治理?
- GraphQL over HTTP 草案内容是否正确标注版本与状态?
- Subscription 是否定义认证续期、超时、重连与事件顺序?
- gRPC 调用是否设置 deadline,流式接口是否定义背压和重连?
- Proto 字段编号是否稳定,删除编号是否已保留?
- gRPC 错误是否使用最具体状态和可解析的结构化详情?
- 是否分别评估 Protobuf 二进制、ProtoJSON、代码和业务兼容?
- 错误是否稳定、可处理且不泄露内部信息?
- 是否执行租户、对象、动作和字段级授权?
- 请求、响应、查询、消息、连接、速率和配额是否有限制?
- 日志、指标和追踪是否脱敏并避免高基数标签?
- 是否具备契约测试、兼容检查、灰度、回滚和弃用计划?
8.1 评审结论
评审记录必须列出接口所有者、契约版本、调用方范围、数据敏感级别、可用性目标和兼容窗口。"不符合"项应记录风险、整改责任人和完成日期;确需例外时,要写明适用范围、补偿控制和退出计划。仅凭口头约定或"内部接口"不能免除安全与兼容要求。
接口上线后应依据真实流量复核设计:监控未使用字段、异常查询、重试放大、错误码分布和旧版本调用。评审不是一次性审批,契约发生破坏性变化、访问范围扩大或数据敏感级别提升时必须重新评审。
9. 参考标准
- RFC 9110:HTTP Semantics:HTTP 方法、状态码和条件请求。
- RFC 9111:HTTP Caching:HTTP 缓存。
- RFC 5789:PATCH Method for HTTP:PATCH 语义。
- RFC 9457:Problem Details for HTTP APIs:标准错误详情,已取代 RFC 7807。
- GraphQL Specification:GraphQL 类型、验证、执行与响应语义。
- GraphQL over HTTP:HTTP 映射草案,采用前确认状态。
- gRPC Core Concepts 与 Error Handling:RPC 与错误模型。
- Protocol Buffers Proto3 Guide:字段编号、存在性和兼容演进。
9.1 核心术语
- 资源:由 URI 标识、通过表示进行交互的业务概念;表示不是资源本身。
- 安全方法:客户端未请求改变服务端状态的方法,不等于没有日志等附带影响。
- 幂等:重复相同请求的预期效果与执行一次相同,不代表响应完全一致。
- 前置条件 :
If-Match等要求服务端仅在条件成立时执行请求。 - 游标:服务端生成、客户端不应解释或修改的分页位置令牌。
- SDL:定义 GraphQL 类型和能力的 Schema Definition Language。
- Resolver:计算 GraphQL 字段值的执行逻辑。
- Deadline:RPC 必须完成的绝对截止点。
- Wire compatibility:新旧消息定义对线上二进制数据的解析兼容,不等同业务兼容。