API 设计中的最小惊讶原则
POST /jobs 返回 200,但任务其实还没开始,这种接口第一次用就会让人警惕。
API 设计里常说“最小惊讶原则”。意思很直接:调用者按常识理解接口时,不应该被结果反手教育。名字像“创建”,就应该真创建;返回“成功”,就别把失败藏到异步回调里;参数叫 timeout,单位就不要一会儿是秒、一会儿是毫秒。
这不是礼貌问题,而是成本问题。接口一旦让人意外,调用方通常就得补上防御性代码:重试、兜底、特殊判断、额外日志。最后最稳定的不是 API,而是围着 API 长出来的一圈补丁。
最常见的惊讶,不在大功能里
让人难受的,往往不是复杂能力,而是小地方的不一致。
同样的动作,不同的返回语义
有些接口成功时返回对象,失败时也返回 200,只是 code 字段不一样。技术上能跑,使用体验很差。HTTP 状态码本来就是给“这次请求是否成功”准备的,再套一层私有状态,相当于让调用者做两次判断。
参数名像一个意思,行为却是另一个意思
force=true 有时表示“跳过检查”,有时表示“覆盖已有数据”,有时甚至表示“即使失败也返回成功”。名字省了,歧义留给别人。
API 里的每个字段,最好都只承担一个稳定含义。字段名不是注释,它本身就是承诺。
幂等性不说清楚
幂等性可以简单理解为:同一个请求重复执行,结果应该可预期,不会越跑越乱。
比如“创建资源”默认往往不是幂等的,调用两次可能生成两份数据;“设置某个状态为开启”通常应该是幂等的,调用十次也还是开启。问题不在于所有接口都必须幂等,而在于文档和命名要把这件事说清楚。否则调用方只能靠猜,或者靠临时测试去试探边界;这类结果是否稳定,未验证。
最小惊讶,不等于最少功能
有时一个 API 看起来“简单”,其实只是把复杂度甩给了调用者。
例如批量接口支持部分成功、部分失败,这很常见,也合理。但如果返回里只给一句 partial success,没有逐项结果,没有失败原因,没有可重试标记,那它看起来简洁,实际最折腾。
最小惊讶原则更像一种分配复杂度的方法:复杂逻辑可以存在,但应该放在调用者能理解、能处理的位置上,而不是藏在模糊语义里。
三个实用检查点
1. 先看名字,再猜行为
把文档先放一边,只看路径、方法、参数名、返回字段。
如果一个没参与设计的人能大致猜对行为,这个接口通常就已经过了第一关。
反过来说,如果必须读完大段说明才能知道 delete 实际只是“标记隐藏”,那名字就有问题。不是功能不能这样做,而是接口不该让人误判。
2. 出错时,能不能立刻知道怎么补救
好的错误信息不是“发生错误”,而是告诉你下一步该做什么。
比如缺参数、权限不足、状态冲突、重试无效,这几类问题的处理方式完全不同。把它们都压成一个通用错误码,只会让调用方写出更大的 if-else。
报错不是为了证明系统有判断能力,而是为了减少来回猜测。
3. 重复调用,结果是否稳定
这个检查很朴素,但很有效。
同样的输入连发两次,会不会多创建一份资源?会不会第一次成功、第二次报错,但实际上状态已经改了?会不会返回格式还不一样?
如果这些问题答不上来,接口多半还没准备好给别人用。
文档不是补锅工具
很多 API 的问题,不是“没写文档”,而是“文档在替坏设计擦屁股”。
当一个接口需要大段“注意事项”来解释例外情况时,更值得先回头看设计本身:是不是命名误导了?是不是默认行为反直觉?是不是把异步、缓存、最终一致性这些系统细节直接暴露给了普通调用者?
真实系统总有边界,最小惊讶不是要求世界绝对整齐。它更像几条基本要求:例外可以有,但要少;复杂可以有,但要明说;默认行为应当尽量符合直觉。
一个简单判断
如果调用者每次写这个接口前都要先翻文档、翻旧代码、翻历史报错,那它大概率已经违背了最小惊讶原则。
更值得继续拆开的问题是:一个接口到底该“严格报错”,还是“尽量帮用户自动纠正”。这件事也很容易做过头。