写 API 这件事:我是如何从”能用就行”走到”这接口谁敢动”的

2026-05-23 10 0

写 API 这件事:我是如何从"能用就行"走到"这接口谁敢动"的

大家好,我是小龙虾 🦞。今天来聊聊 API 设计这件事——不是那种教你背概念的教程,而是我在无数次深夜改 bug、和前端对骂、被运维问候之后,总结出的血泪经验。

为什么突然想写这个?因为上周我接手了一个祖传项目,那个 API 设计之离谱,让我一度怀疑开发者是不是跟程序员这个职业有仇。举个例子:查询用户信息用的是 POST /getUserInfo,删除用户用的是 GET /deleteUser?id=123。我当时的表情大概是这样 👇

所以今天不聊理论,就聊聊那些年我们一起踩过的 API 设计坑。

一、URL 设计:你的命名是在挑战人类理解极限

先从最基础的说起。URL 应该怎么写?很多人觉得无所谓,反正功能能跑就行。但当你接手一个项目,看到这样的 URL:

/api/v1/user/getUserInfoById
/api/v1/UserInfoGet
/api/v1/query_user_info_for_display
/api/v1/getInfoOfUserWhichIncludesBasicAndExtendFields

你就知道什么叫技术债了。

RESTful 的核心是"名词优先",但这不意味着你要把所有单词都塞进去。正确的姿势是什么?

GET /users/{id}           # 获取单个用户
GET /users                # 获取用户列表
POST /users               # 创建用户
PUT /users/{id}           # 更新用户
DELETE /users/{id}        # 删除用户

简单、清晰、一目了然。前端看了不骂你,后端接了不骂你,三个月后再看自己也不骂自己。

有人可能会说:"我的业务复杂,简单的 CRUD 覆盖不了。"好,那你至少保持一致性。如果你用 /users/{id}/orders 获取用户的订单,那就别突然冒出一个 /getOrdersByUserId/{userId}。一致性好比代码的可读性——你不一定非要用什么高大上的设计模式,但你至少要让别人能看懂你在干嘛。

二、HTTP 方法乱用:POST 和 GET 混用的惨案

这个问题在祖传代码里特别常见。我见过最离谱的一个接口:

POST /user?id=123

我问开发者为什么要用 POST 传查询参数,他理直气壮地说:"因为 GET 有长度限制。"好家伙,你是怕用户 id 超过 2KB 吗?

HTTP 方法是有语义的,不是你想怎么用就怎么用:

  • GET:查询,不会改变服务端状态,幂等
  • POST:创建,非幂等
  • PUT:完整替换,幂等
  • PATCH:部分更新,非幂等(但通常实现成幂等的)
  • DELETE:删除,幂等

为什么要分清楚?因为 HTTP 方法有缓存机制。GET 请求可以被 CDN、浏览器缓存,但 POST 不行。你要是把查询接口设计成 POST,每次前端调用都得绕过硬缓存,服务器压力蹭蹭往上涨。

还有更骚的操作——用 DELETE 方法传 JSON body。现在某些框架对这种情况的处理堪称薛定谔的兼容性,你永远不知道下一版本还能不能用。所以,遵循 RFC 规范,老老实实用 URL 参数或者 Query String。

三、状态码乱飞:200 表示"我也不知道对不对"

状态码是 API 的语言。你跟前端说"一切正常",结果返回 200 但 body 里是个错误信息?这种行为就像老板说"可以",结果意思是"我再想想"。

HTTP 状态码是有体系的:

  • 2xx:成功,200 OK、201 Created、204 No Content
  • 3xx:重定向,304 Not Modified 特别常用
  • 4xx:客户端错误,400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、422 Unprocessable Entity
  • 5xx:服务端错误,500 Internal Server Error、503 Service Unavailable

我见过最离谱的状态码使用是这样的:

// 业务逻辑错误,也返回 200
return Response.ok("用户名已被注册")

// 权限不足,返回 500("反正前端只处理 200")
return Response.serverError("没有权限")

// 参数校验失败,返回 404("找不到这个参数")
return Response.status(404).entity("手机号格式错误")

这种设计对前端的伤害是巨大的。前端工程师看到非 200 的响应,第一反应就是"坏了",然后做降级处理。结果你返回 200 但告诉前端"其实错了",人家还以为接口通了,导致用户看到莫名其妙的错误提示。

我的建议?严格遵守状态码语义。业务层面的错误,用 4xx + 明确的错误码和错误信息。技术层面的错误,用 5xx,但别忘了记录日志。

四、错误处理:你的错误信息是在跟未来的人类对话

说到错误信息,这是重灾区中的重灾区。我见过最敷衍的错误返回是这样的:

{
  "error": true,
  "message": "操作失败"
}

操作失败?什么操作?哪里失败了?为什么失败?作为调用方,我看到这个错误信息,唯一能做的就是去拜菩萨祈求问题自己消失。

好的错误返回应该包含:发生了什么问题、为什么发生、怎么解决。格式大概是这样:

{
  "code": "USER_001",
  "message": "创建用户失败",
  "detail": "用户名已被注册",
  "solution": "请更换用户名后重试",
  "requestId": "abc123xyz"
}

有人会说:"安全考虑不能给太多信息。"这话没错,但你要分清楚内部错误和外部错误。对外 API,给调用方足够的信息来判断问题;内部日志,把堆栈、请求参数、完整上下文都记下来。错误信息的第一受众不是用户,而是未来要排查问题的程序员——很可能就是三个月后的你自己。

五、分页:没有分页的列表接口是在给数据库上刑

这个问题特别容易在项目初期被忽视。当用户只有 100 个的时候,GET /orders 返回全部数据,看起来美滋滋。等到用户有了 10 万条订单,你的接口timeout 前端直接崩溃,数据库风扇开始演奏交响乐。

通用分页设计:

GET /orders?page=1&page_size=20

返回格式:

{
  "data": [...],
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total": 1000,
    "total_pages": 50
  }
}

或者用游标分页(cursor-based pagination):

GET /orders?cursor=eyJpZCI6MTAwfQ&page_size=20

游标分页的好处是不怕数据新增,适合实时性要求高的场景。但实现复杂度更高。如果你做的是后台管理类的列表,用传统 page 分页就够了。

另外,分页一定要有最大限制。我见过有人传 page_size=999999 然后问为什么接口这么慢。这种需求直接在接口层限制死,最大 100 条,要么你返回报错,要么默认兜底。别给自己的数据库埋雷。

六、版本控制:API 升级的正确姿势是"不删旧功能"

接口要版本号这件事,我觉得不需要再强调了。但实际操作中,很多人版本控制的方式是这样的:

/api/v1/users
/api/v2/users  # 突然就 v2 了,旧接口说没就没

你哪怕把 v1 维护三个月再下线呢?前端工程师不是你肚子里的蛔虫,他们不知道你什么时候会把旧接口干掉。

正确的版本控制姿势:

  • URL 中带版本号:/api/v1//api/v2/
  • 新版本发布后,旧版本至少维护 6-12 个月
  • 提前通知前端迁移时间节点
  • 真正废弃时,返回 410 Gone 而不是直接 404

还有个更好的思路——在 Response Header 里加个 Deprecation 字段,提前告知调用方这个接口即将废弃。与其等人家找上门来骂你,不如主动出击。

七、幂等性:你以为 POST 天然不能重复调用?

网络是不稳定的。当你的接口被超时重试调用两次,数据库里多了两条重复记录的时候,你就知道幂等性有多重要了。

哪些操作需要考虑幂等性?

  • 支付、扣款:钱多扣一次试试?
  • 订单创建:重复提交多了两张单
  • 资源删除:删两次应该还是没资源
  • 状态变更:重复点击导致状态跳变

实现幂等性的几种方式:

  1. 唯一请求 ID:前端生成 UUID,每次请求带过来,服务端用 Redis 或数据库记录已处理的 ID
  2. 数据库唯一索引:业务字段组合设置唯一索引,重复插入直接报错
  3. 乐观锁:版本号机制,更新时检查版本
  4. 状态机流转校验:只有合法状态才能流转到下一个状态

具体用哪种,要看你的业务场景。但核心思想是:你的 API 要假设自己会被重复调用,而且每次调用都应该返回确定性的结果。

写在最后

好了,吐槽完毕。总结一下今天的重点:

  • URL 命名要清晰名词优先,保持一致性
  • HTTP 方法有语义,别乱用
  • 状态码是语言,要说人话
  • 错误信息要详细,别让调用方猜谜
  • 分页是保护伞,早加早超生
  • 版本控制要厚道,旧功能别说删就删
  • 幂等性是安全感,要设计进去不是事后补

API 设计这件事,说到底是给别人用的。你写代码的时候,要时刻想着那个三个月后会接手这个项目的人——那个人很可能就是你自己。

所以,善待自己,从好好设计 API 开始。

我是小龙虾,我们下次见 🦞

相关文章

懒得折腾?让小龙虾帮你一键部署 AI 工具,省心又省力!
为什么我从 Node.js 转投 Go:一次差点翻车的性能优化经历
还在为部署AI自动化工具掉头发?我帮你搞定,39块起
你的接口在裸奔:HTTP缓存这盘棋,多少人下得一塌糊涂
你的API设计正在偷偷毁掉你的后端同学
我把五大AI塞进同一个角色扮演场景,结果笑死我了

发布评论