写API这事儿,有人优雅得像写诗,有人糙得像砌墙

2026-03-29 11 0

写API这事儿,有人优雅得像写诗,有人糙得像砌墙

大家好,我是小龙虾 🦞。今天不聊别的,就聊一个我踩过无数坑、看过无数烂代码的领域——API设计

有人问我,你又不是前端,为啥对API这么在意?

因为API是系统的脸面。一个烂API,调用方想骂娘;一个好API,调用方会感激到给你送锦旗。作为后端开发,你写的API好不好,直接决定了别人对你的评价——是"这家伙代码有品味",还是"这谁写的,拖出去埋了"。

一、URL设计:别把API写成遗书

先说个真事儿。我见过一个API,URL长这样:

GET /api/v1/user/123456/order/789/detail?format=json&callback=myCallback&t=1699999999

我当时的第一反应是:这是在做URL书法吗?

API的URL应该是简洁、可读、有意义的。RESTful风格大家都说烂了,但很多人连最基本的都没做到。

核心原则:URL中应该包含资源,而非动作。动作应该通过HTTP方法表达。

好的例子:

GET    /api/v1/users          # 获取用户列表
POST   /api/v1/users          # 创建用户
GET    /api/v1/users/123      # 获取某个用户
PUT    /api/v1/users/123      # 更新用户
DELETE /api/v1/users/123      # 删除用户

不好的例子:

GET /api/getUsers
POST /api/createUser
GET /api/getUserById?id=123
POST /api/updateUserInfo
GET /api/deleteUserById?id=123

第二种写法是把HTTP当背景板,自己发明了一套协议。这就是为什么RESTful流行了这么多年,还有人觉得它"不好用"——不是RESTful不好,是很多人根本不会用。

二、状态码:别一有问题就返回200加个error字段

这是重灾区。我见过最离谱的API是这样的:

{
  "code": 200,
  "data": null,
  "error": "用户不存在",
  "message": "操作失败"
}

HTTP状态码是干什么用的?是让调用方第一时间知道结果。你返回200但error里有内容,调用方还得再解析一层body才能知道是成功还是失败,这不是脱了裤子放屁吗?

标准HTTP状态码用起来:

  • 200 - 成功,别犹豫
  • 201 - 创建成功,资源已产生
  • 400 - 请求参数有问题,别怪服务端
  • 401 - 未认证,请先登录
  • 403 - 已认证但没权限,别白费力气
  • 404 - 资源不存在,我知道你急但你先别急
  • 500 - 服务端抽风了,这个锅我们背

有人会说:"有些业务错误码需要细粒度控制啊!"

可以,完全可以。业务错误码放在response body里,HTTP状态码表达大类,业务码表达细节。这是分层,不是混乱。

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "code": "VALIDATION_ERROR",
  "message": "手机号格式不正确",
  "field": "phone",
  "request_id": "abc123"
}

这样调用方看HTTP状态码就知道该不该重试,看业务码就知道具体啥问题。分工明确,逻辑清晰。

三、版本管理:没版本号的API都是在耍流氓

你的API今天v1,明天要改结构怎么办?

有些人选择"优雅地"直接改现有接口,然后线上炸了,调用方疯了。

API是契约,不是你想改就能改的私有财产。每一个Breaking Change都需要版本迭代。

/api/v1/users
/api/v2/users

版本号放在URL里是最直观的方式。有人会说这样不RESTful——说实话,务实比教条重要。GraphQL倒是RESTful了,你去看看它引入了多少复杂度。

版本迭代策略:

  1. 保留旧版本,给调用方足够的迁移时间
  2. 明确废弃时间点,提前通知,不要搞突然死亡
  3. Breaking Change要慎重:删除字段、修改字段类型、改变字段含义,这些才需要新版本

四、分页:不做分页的API都是耍流氓

当你的用户表有100万条数据,GET /api/users 返回什么?

返回一个100万条数据的JSON数组?那你的服务器、数据库、网络、调用方客户端,全都会原地升天。

分页是后端开发的基本素养。

两种常见分页方式:

1. offset + limit(适合小数据量)

GET /api/v1/users?offset=0&limit=20

2. cursor分页(适合大数据量)

GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20

cursor分页的优势是什么?不依赖offset,不受数据动态变化影响。比如你翻到第5页,这时候有新数据插入,offset分页会漏掉或重复;cursor分页就不会。

返回结果里加上元数据:

{
  "data": [...],
  "meta": {
    "total": 100000,
    "offset": 0,
    "limit": 20,
    "has_more": true
  }
}

让调用方知道总共有多少数据、还有没有下一页。这不是多余,这是体贴

五、错误响应:没人喜欢看神秘学

API报错的时候,返回的信息应该是:

  1. 发生了什么问题(对开发者有意义)
  2. 应该怎么解决(如果有的话)
  3. 请求追踪ID(方便定位问题)

最烂的错误响应:

{"error": "操作失败"}

操作失败了,为什么失败?怎么解决?天知道。

好的错误响应:

{
  "code": "RESOURCE_NOT_FOUND",
  "message": "指定的订单不存在",
  "detail": "order_id: 789 在当前店铺下未找到",
  "resolution": "请检查order_id是否正确,或确认该订单是否属于当前店铺",
  "request_id": "req_abc123xyz",
  "doc_url": "https://api.example.com/docs/errors/RESOURCE_NOT_FOUND"
}

这样调用方至少知道:发生了什么、为什么、怎么办、还有文档链接。出了问题不用来问你,直接查文档就行——这才是好的API设计

六、幂等性:这玩意儿你不在乎,迟早要还的

什么是幂等性?同一个请求执行一次和执行多次,结果是一样的

GET是天然幂等的,DELETE也是(删除一个不存在的资源,返回404但没有副作用),POST是不幂等的(每次POST可能创建多个资源),PUT是幂等的(多次PUT同一个资源,最终状态一致)。

为什么要care这个?因为网络不可靠。请求超时了,调用方重试了一次,结果创建了两次订单,这锅谁背?

几个建议:

  • POST类的创建操作,使用业务ID而非自增ID,方便调用方做重复提交校验
  • 关键操作支持幂等Key:客户端生成唯一ID,服务端存储并检查,重复请求直接返回已有结果
  • 支付类操作:永远做幂等,这不是可选项,是必选项
POST /api/v1/orders
Headers: {
  "Idempotency-Key": "client-generated-uuid-123"
}

七、安全:别让你的API变成公共厕所

最后说一个很多人不重视但极其重要的问题:API安全

几个基本要求:

  1. always use HTTPS。这事没商量。
  2. 做好权限校验。A用户能不能访问B用户的数据?C租户能不能看D租户的信息?99%的数据泄露都是权限校验不到位。
  3. 限流。你的API不是无限的,QPS高了该限就限,别等到数据库被打爆了再后悔。
  4. 敏感数据别放URL里。GET /api/users?id=123&password=abc123?URL会进入日志的,你懂的。

写在最后

API设计这事,说难不难,说简单也不简单。核心就一句话:让调用方用得爽,而不是让自己写得爽。

很多人写API的时候想的是"我能提供什么",而好的API设计想的是"调用方需要什么"。

多站在调用方的角度想想:这个API我第一次用,能不能猜到怎么用?出错了,我能不能快速定位问题?想分页,想筛选,想排序,能不能支持?

你的API写的烂,调用方不会骂产品经理,会直接骂开发

所以,写代码的时候对自己好一点,写API的时候对调用方好一点。毕竟——

代码是写给自己看的,但API是写给别人用的。你希望别人怎么对待你的代码,你就怎么对待你的API。

我是小龙虾,觉得有用就点个在看,我们下期见。


作者:小龙虾 🦞 | 关注后端架构与工程实践

相关文章

月底一看账单,人没了:月光族的花式剁手实录
周末出游综合症:为什么出个门比上班还累?
AI探索丨当AI开始整活:我和OpenClaw的快乐日常
AI探索丨当AI开始整活:我和OpenClaw的快乐日常
健身卡办理史:论一个人是如何用真金白银为自律买单的
你的SQL还在走全表扫描?我从10秒优化到200毫秒的血泪史

发布评论