「你们这个接口,成功的时候返回 data 是对象,失败的时候 data 是空字符串,我前端得写三层判断。」
这是三年前一个前端同事在群里发的原话。当时我还有点不服气,觉得「加个判断而已」。后来我自己去对接了几个外部接口,才彻底明白那种感受——接口设计的所有偷懒,最后都变成调用方代码里的 if。
这篇不讲 RESTful 那套大道理,只讲一些具体到让人抓狂的细节。
类型不要摇摆
同一个字段,在不同情况下返回不同类型,是最招人恨的设计,没有之一。常见变体:
- 列表为空时返回
null,有数据时返回数组。调用方必须每次判空。空列表就返回[]。 - 失败时把 data 从对象改成空字符串(就是上面那个例子)。
- 数字有时是
123,有时是"123",取决于底层某个序列化配置。
顺便说个相关的:大整数 id 建议用字符串传。JavaScript 的 Number 只能精确表示到 2 的 53 次方,雪花算法生成的 19 位 id 传过去就变成末位是 0 的错误值了。这个问题我们线上出过一次,表现是「用户点详情页打开的是另一个订单」,排查了半天才想到是精度问题。
时间格式要一次说清
我见过一个系统里同时存在四种时间格式:秒级时间戳、毫秒级时间戳、2026-08-25 10:30:00、以及带 T 和 Z 的 ISO 格式。
我的建议是全站统一,并且一定要带时区信息。2026-08-25 10:30:00 这种裸字符串是有歧义的——服务器在东八区,调用方在别的时区,谁也不知道这个 10:30 是谁的 10:30。我们吃过一次亏,一个跨时区的对账任务差了 8 小时的数据。
错误信息要给调用方能用的东西
「操作失败,请重试」是最没用的错误信息。调用方拿到它,既不知道该不该重试,也不知道怎么告诉用户。
我现在的做法是错误响应里包含三部分:
code:给程序判断的稳定标识,比如COUPON_EXPIRED。用字符串而不是数字,因为数字需要查表,字符串自解释。message:给开发者看的详细描述,可以包含具体数值,比如「优惠券于 2026-08-20 过期」。display_message(可选):可以直接展示给终端用户的文案。
另外,错误码一旦发布就不能改含义。可以新增可以废弃,但不能让 1003 从「余额不足」变成「参数错误」。我见过因此导致下游错误重试的事故。
分页要说清楚有没有总数
调用方需要知道:还有没有下一页?总共多少条?这两个问题的答案成本差很多——has_more 几乎免费(多查一条判断即可),total 在大表上可能要额外一次昂贵的 count。
所以我的做法是:has_more 永远返回,total 由调用方通过参数显式索取。别默认返回 total,我见过一个列表接口 80% 的耗时花在没人看的 count 上。
区分「字段没传」和「字段传了 null」
更新接口最经典的坑。用户提交了 {"nickname": null},是想把昵称清空,还是不想改昵称?如果你用的是「只更新非空字段」的逻辑,那用户永远没法清空昵称。
解决方案有几种:用 PATCH 语义配合明确的字段列表;或者提供专门的清空接口;或者用一个包装类型区分「未设置」和「设置为空」。不管选哪种,文档里必须写清楚。
不要指望文档里那句「请勿重复调用」
网络会超时,客户端会重试,这是物理规律。写一句「请勿重复调用」在工程上等于没写——接口自己必须能扛住重复。
枚举值要考虑新增
你今天定义订单状态有 5 个值,明天加了第 6 个。所有调用方的 switch 语句都会走到 default 分支。
所以:文档里明确声明「枚举值可能新增,请做好兜底」,新增前提前通知。我们有过一次事故——下游把未知状态当成了「已取消」,一批正常订单被误判。
最后
我判断一个接口设计得好不好,有个很土的方法:假装自己是调用方,把对接代码写一遍。写的过程中每出现一次「这里得判断一下」的念头,就记一笔。超过三笔,这个接口就该重新设计。
这个方法花不了半小时,但比任何设计规范都管用。因为规范是抽象的,而调用方的痛苦是具体的。