API 不是控制器方法的外包装,而是服务与调用方共同依赖的长期契约。一个接口能返回正确 JSON 只是起点;资源命名、HTTP 语义、幂等、错误结构和兼容策略,决定它能否被稳定使用和逐步演进。
问题背景:API 是长期契约
接口发布后,服务端通常无法同时升级所有调用方。移动端可能数月不更新,脚本可能由其他人维护,合作服务也有自己的发布窗口。因此,设计应把“不破坏已有客户端”视为默认约束。
先写出调用方要完成的任务和资源生命周期,再决定端点。不要直接把内部函数暴露成 /createOrder、/updateOrderStatus。内部实现会重构,资源语义应更稳定。
从资源而不是动作建模
资源用名词表示,集合与单个资源保持一致:
01GET /v1/orders02POST /v1/orders03GET /v1/orders/{orderId}04PATCH /v1/orders/{orderId}05DELETE /v1/orders/{orderId}
复杂动作可以建模为子资源或状态变更。例如取消订单可以是 POST /v1/orders/{id}/cancellations。这比无限扩张动词端点更容易表达“取消记录本身也有状态和原因”。
资源标识应稳定,不暴露会变化的数据库自增顺序或敏感业务含义。请求与响应字段属于外部契约,不必与数据库列一一对应。
HTTP 方法、状态码与幂等
GET 应只读取;PUT 表示用完整表示替换资源,天然适合幂等;PATCH 表示部分修改;POST 常用于创建或触发非幂等处理。幂等不是“请求只执行一次”,而是重复相同请求后,对外可观察结果与执行一次一致。
创建成功返回 201 Created 并通过 Location 指向资源;异步受理可返回 202 Accepted;无响应体的成功操作返回 204 No Content。客户端错误与服务端错误要区分,不能把所有异常都包装成 HTTP 200。
分页、筛选和错误结构
数据量较小且稳定时可以使用页码;频繁新增的大集合更适合基于稳定排序键的游标分页。游标应被视为不透明字符串,调用方不应解析内部字段。
01{02 "items": [03 { "id": "ord_7f3", "status": "pending", "total": 12800 }04 ],05 "page": {06 "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTI0In0",07 "hasMore": true08 }09}
金额示例使用最小货币单位,避免浮点误差。错误响应要给机器稳定的 code,给人可读但不泄露内部细节的 message,并提供关联请求的 requestId。
01{02 "error": {03 "code": "ORDER_STATE_CONFLICT",04 "message": "当前订单状态不允许该操作",05 "requestId": "req_01J2..."06 }07}
字段校验错误可以增加 details,但不要返回 SQL、堆栈、文件路径或令牌。
实践:订单 API
下面的 Go 示例展示创建入口的关键边界。它只用于教学,省略了真实数据库和鉴权实现。
01type CreateOrderRequest struct {02 ProductID string `json:"productId"`03 Quantity int `json:"quantity"`04}0506func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {07 key := r.Header.Get("Idempotency-Key")08 if key == "" {09 writeError(w, 400, "IDEMPOTENCY_KEY_REQUIRED")10 return11 }1213 var input CreateOrderRequest14 if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&input); err != nil {15 writeError(w, 400, "INVALID_JSON")16 return17 }18 if input.ProductID == "" || input.Quantity < 1 || input.Quantity > 99 {19 writeError(w, 422, "VALIDATION_FAILED")20 return21 }2223 order, created, err := h.service.Create(r.Context(), key, input)24 if err != nil {25 writeServiceError(w, err)26 return27 }28 if created {29 w.Header().Set("Location", "/v1/orders/"+order.ID)30 w.WriteHeader(http.StatusCreated)31 }32 json.NewEncoder(w).Encode(order)33}
限制请求体可以避免无界内存占用;业务校验与 JSON 语法错误使用不同状态;真正的幂等保证必须在共享持久层和订单写入之间建立原子边界,不能只用单机内存 Map。
版本演进与兼容
优先做兼容性扩展:增加可选字段、增加新端点、让客户端忽略未知字段。删除字段、改变类型、修改枚举含义通常是破坏性变更。版本可以放在路径中,但 /v2 不是随意破坏兼容的许可证。
发布前应使用契约测试覆盖典型请求、边界输入和错误结构;记录弃用时间线;通过指标观察旧版本流量,而不是凭猜测下线。
安全、性能与总结
认证后仍要验证资源归属,列表端点要限制分页大小,写操作要设置超时并传播取消信号。日志记录请求 ID、结果和耗时,但不要记录密码、完整 Token 或敏感正文。
清晰的 REST API 建立在稳定资源、准确 HTTP 语义、可恢复错误和兼容演进之上。延伸学习可以进入认证授权、缓存一致性、服务分层与契约测试。