接口规范:RESTful & RPC

接口是SDK的骨架。你写的接口好不好用,直接决定了对接方的「体感」。我这些年看了太多因为命名混乱、状态码写得随意,导致双方扯皮的案例。今天咱们就把这块掰开揉碎讲清楚。

接口命名规范

先说RESTful。我个人的习惯是:资源用名词复数,动作交给HTTP动词。比如获取用户列表,就是 GET /users;创建用户是 POST /users;更新某个用户是 PATCH /users/{id}。别搞出 /getUserList 这种RPC味十足的路径。

常见的命名误区
  • 动词混在路径里:/deleteUser?id=xxx → 应该用 DELETE /users/{id}
  • 路径层级混乱:/users/info/orders → 应该用 /users/{userId}/orders
  • 单词单复数随意:一会 /order 一会 /orders → 统一用复数

RPC风格的接口,我一般建议不要和RESTful混用。如果你项目里定了RPC协议,那就用 动词+名词 的驼峰命名,比如 createUser、queryOrderDetail。嗯,这里要注意:RPC接口的参数必须稳定,不要随意增删字段,否则老版本客户端会崩。

请求/响应结构

我见过最糟心的事——同一个SDK,三个接口返回的JSON结构都不一样。有的字段叫 code,有的叫 status,有的干脆不包装直接返回数组。你说对接方看了头不头痛?

我建议统一采用以下结构:

// 请求体示例(POST /users)
{
  "username": "张三",
  "email": "zhangsan@example.com",
  "age": 28
}

// 响应体示例
{
  "code": 0,          // 业务状态码,0表示成功
  "message": "success",
  "requestId": "e3b0c442-...",  // 追踪ID,方便排查
  "data": {           // 真正的业务数据
    "userId": 1001,
    "username": "张三"
  }
}

为什么要在响应里加 requestId?我曾经被线上问题折腾过:客户端报错,但日志里找不到对应的请求。加上这个字段后,运维拿着ID去链路追踪系统一搜,问题秒现。你想想看,一个字段省了多少debug时间。

请求头推荐
  • Content-Type: application/json
  • Authorization: Bearer <>(如有鉴权)
  • X-Request-Id(客户端生成,幂等使用)
  • Accept-Language(国际化场景)

状态码定义

HTTP状态码和业务状态码,两者要分清楚。HTTP状态码只管网络层面的成功失败,比如200、400、500。业务状态码放在响应体 code 字段里,告诉客户端这次请求的业务结果。

HTTP状态码 含义 常见场景
200 成功 正常返回数据
400 请求参数错误 缺少必填字段、格式错误
401 未授权 过期或无效
403 无权限 资源访问被拒绝
404 资源不存在 查询的ID不存在
429 频率限制 API调用超限
500 服务端内部错误 数据库异常、未捕获异常
503 服务暂不可用 维护中、过载
避坑提醒 我曾经见过把数据库查询失败的异常堆栈直接返回给客户端,HTTP状态码还写的是200。后果?客户端收到了 {"code":0,"data":"SQL Error: ..."},以为请求成功了……这种问题一定要在网关层统一拦截,业务异常返回对应的400或500,同时掩盖内部细节。

分页规范

列表接口必须做分页。不做分页?你想想看,用户表1000万条数据,一次查出来,服务端内存先爆掉。我建议统一采用 基于偏移量的分页(page + pageSize),如果数据量大且实时性高,可以用游标分页(cursor + limit)。

请求参数:

GET /users?page=1&pageSize=20
GET /users?cursor=abc123&limit=20

响应体:

{
  "code": 0,
  "message": "success",
  "requestId": "...",
  "data": {
    "items": [...],        // 当前页数据
    "total": 1234,         // 总记录数(偏移量分页需要)
    "page": 1,
    "pageSize": 20,
    "hasMore": true         // 游标分页用
  }
}

注意 page 从1开始还是从0开始?我个人习惯从1开始,这样对接方比较好理解。但不管从几,必须在文档里明确写出来。还有pageSize要限制最大值,我一般设成100,超过就截断。

错误码设计

错误码是我最想吐槽的地方。很多团队设计错误码,一种是全用0和1,一种是弄几十个数字没有文档。其实错误码设计的核心是:可读、可分类、可扩展。

我常用 业务领域 + 错误类型 + 具体编号 的结构,比如:

// 格式:AABBBCCC
// AA = 业务领域(01用户,02订单,03支付...)
// BBB = 错误类型(001参数错误,002鉴权错误,003资源不存在...)
// CCC = 具体编号(001~999)

示例:
01001001 => 用户模块,参数错误,手机号格式不正确
02003001 => 订单模块,资源不存在,订单ID无效
03002001 => 支付模块,鉴权错误,余额不足

这样做的好处是什么?客户端可以根据错误码前缀就知道是哪个模块出的问题,而且新加错误码不会跟旧的冲突。

错误码设计原则
  • 0 永远表示成功,非0表示失败
  • 错误码不可变:一旦发布,不能修改含义,只能废弃或重编号
  • 错误信息要友好,但不能暴露内部实现:比如「数据库连接池耗尽」改成「系统繁忙,请稍后重试」
  • 错误信息里包含 requestId,方便运维查日志

我们来看一个完整的错误响应示例:

{
  "code": 1002001,
  "message": "用户不存在",
  "requestId": "a1b2c3d4-...",
  "data": null
}

说到这里,我想起之前有个合作方,他们的错误码全部用 1、2、3……一个数字对应一种错误,文档只写了「1:错误 2:错误」。呵呵,排查问题只能靠猜。后来我建议他们改成结构化编码,对接效率翻倍。

接口规范知识体系 命名规范 RESTful资源·RPC动词 请求/响应结构 统一信封·requestId 状态码定义 HTTP码·业务码 错误码设计 结构化·可扩展 分页规范 偏移量·游标·限制pageSize 请求头规范 Content-Type / X-Request-Id 业务状态码 0=成功 / 领域分类 错误信息 友好·不暴露内部 幂等/追踪 requestId + idempotent 核心原则:统一、稳定、可追踪 命名定义一旦发布,变更需版本升级
经验总结 说白了,接口规范定得好,后面联调省一半时间。我建议每个SDK交付前,团队按「命名→结构→状态码→分页→错误码」这条线做一次合规审查,跑一遍自动化测试。我曾经吃过亏:分页字段名 pageIndex 和 pageNo 混用,导致前端换了三个版本才统一。从那以后,我就在项目初期写死了这份规范,谁也别想擅自改。