写API这5年,我最后悔没早知道的那些坑

2026-06-13 11 0

干过后端开发的都知道,写业务代码其实不算难,真正的难点在于接口设计。一个烂接口可以让你后续维护时恨不得穿越回去把写它的人打一顿——然后发现自己就是那个人。

今天来聊聊我这些年踩过的坑,以及怎么绕过去。纯属个人血泪史,拿出来给大家当反面教材。


1. HTTP方法乱用,你以为POST和PUT真的没区别?

先问个问题:更新用户信息,用POST还是PUT?

很多人会说"随便,反正功能一样"。错,大错特错。我见过有人全站更新操作全用POST,问就是"习惯了"。

正确姿势:

GET    /users/123      # 查看用户
POST   /users          # 创建用户
PUT    /users/123      # 完整更新用户(幂等)
PATCH  /users/123      # 部分更新用户(非幂等但可重试)
DELETE /users/123      # 删除用户

幂等性是个很重要的概念。PUT是幂等的,调用一次和调用十次结果一样;POST不是幂等的。你要是把订单支付这种操作用PUT,等着被投诉吧——用户疯狂点支付,接口给你重复扣款十次,哭都来不及。

还有个经典的:搜索用GET还是POST?答案是GET。URL参数、Query String,天经地义。POST适合那种请求体比较复杂的场景,比如上传文件、发送大JSON。搞反了的话,浏览器后退按钮都能给你整出bug来。


2. 状态码乱返回,200表示"我成功了"?太天真

有人返回200,但code字段写着400,问我为什么前端一直报接口失败。我:???

标准HTTP状态码是给谁看的?是给HTTP层看的,是给CDN、网关、反向代理看的。你在里面塞个业务层的错误码,前面的基础设施全傻了,不知道你这接口到底成没成功。

最常用状态码记住这张表就够了:

2xx - 成功
  200 OK                    # 标准成功
  201 Created              # 创建资源成功
  204 No Content           # 成功但没返回内容(比如DELETE)

4xx - 客户端错误
  400 Bad Request          # 请求参数有问题
  401 Unauthorized        # 没登录
  403 Forbidden           # 没权限
  404 Not Found           # 资源不存在
  409 Conflict            # 冲突(比如重复创建)
  422 Unprocessable Entity # 参数格式对但语义错
  429 Too Many Requests   # 请求太快了,歇会儿

5xx - 服务器错误
  500 Internal Server Error  # 代码bug
  502 Bad Gateway            # 依赖服务挂了
  503 Service Unavailable    # 服务不可用
  504 Gateway Timeout         # 超时

特别想说一下401和403的区别。很多人混用,但实际上:401是"你还没认证,我需要你证明身份";403是"你身份验证过了,但你没权限干这事"。搞反了前端登录逻辑会乱成一锅粥。

还有个新手常犯的:把所有错误都返回200,然后在body里写{code: 500, message: "服务器错误"}。你这是欺负HTTP层的中间件都不识字吗?网关看到200就认为成功了,然后你的"错误"就这么静默通过了。


3. 分页设计反人类,第0页是什么鬼?

很多API的分页参数是这样的:

GET /users?page=0&size=20

程序员是从0开始数数的,但产品经理、测试、甲方爸爸不是。"你给我看第0页?数据呢?"然后你得解释半天为什么从0开始。

更科学的方案是用游标分页(Cursor Pagination)

GET /users?cursor=eyJpZCI6MTIzfQ&limit=20

返回:
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTQzfQ",
  "has_more": true
}

游标分页的好处:

  • 不管数据怎么变化,新增数据不会导致重复或遗漏
  • 前端不用算页码,"有没有下一页"一目了然
  • 性能稳定,不随"页数"增大而变慢

如果非要用工号分页,用偏移量分页时建议用offset而不是page,至少语义清晰:

GET /users?offset=40&limit=20  # 从第40条开始,取20条

4. 路径设计随心所欲,RESTful是什么可以吃吗?

见过最离谱的接口:

POST /getUserInfo
POST /user/get_info
POST /queryUserById
POST /fetchUserData

同一个功能,四种写法。这要是接手别人的代码,能把人逼出高血压。

RESTful规范其实没那么复杂:

# 资源用名词,不是动词
GET    /articles          # 文章列表
GET    /articles/123     # 单篇文章
POST   /articles         # 创建文章
PUT    /articles/123     # 更新文章
DELETE /articles/123     # 删除文章

# 嵌套资源表达关系
GET    /users/123/articles        # 这个用户的所有文章
GET    /users/123/articles/456  # 具体某篇

# 查询参数做过滤/排序/分页
GET    /articles?status=published&sort=created_at&order=desc

有人问:批量操作怎么办?比如同时删除100个用户。标准REST没有批量操作的规范,但业界通用的做法是:

POST /users/batch
Body: { "ids": [1, 2, 3, ..., 100], "action": "delete" }

或者用一种更"REST"的方式:

POST /users/batch-delete
Body: { "user_ids": [1, 2, 3...] }

别纠结,实用第一。


5. 错误信息形同虚设,"操作失败"能再敷衍点吗?

这个星球上最让人崩溃的报错是什么?

{
  "code": 400,
  "message": "操作失败"
}

操作失败是什么鬼?为什么失败?是我传参错了还是服务器炸了?用户看到这种错误,99%会疯狂重试,然后你的系统压力翻倍,然后真·服务器炸了。

好的错误响应应该长这样:

{
  "code": 422,
  "message": "参数校验失败",
  "errors": [
    {
      "field": "email",
      "message": "邮箱格式不正确",
      "value": "this is not email"
    },
    {
      "field": "age",
      "message": "年龄必须大于0",
      "value": -1
    }
  ],
  "request_id": "req_abc123"  # 方便排查的请求ID
}

几个关键点:

  • 明确指出是哪个字段出错,前端可以精准定位到输入框
  • 说清楚错误原因,不是"非法"而是"邮箱格式不正确"
  • 带上出错的具体值,方便复现问题
  • 加上request_id,出问题时日志搜索一把梭

还有个大坑:生产环境的错误信息不要包含敏感信息(比如SQL语句、内部文件路径、堆栈信息),但测试/预发环境可以详细些——方便开发调试。区分环境,这点很重要。


6. 版本管理裸奔,v1/v2/v3到底是什么?

API要升级,但老接口不能动,怎么办?版本化。

常见的版本化方式:

# URL版本(GitHub、Stripe在用)
GET /v1/users
GET /v2/users

# Header版本(更RESTful,但不够直观)
GET /users
Accept: application/vnd.myapp.v2+json

# Query参数(简单但不够规范)
GET /users?version=2

我的建议:用URL版本。原因很简单——调试方便。curl直接敲,不用加header,浏览器直接输地址,不用改Accept头。 Stripe的API文档满天飞,但凡用过Stripe的人都夸它文档好,其中一个原因就是版本在URL里,一目了然。

版本发布节奏建议:

  • 新版本发布后,老版本至少保留6-12个月
  • 每个版本有明确的EOL(end of life)日期
  • 重大Breaking Change必须升版本
  • 小功能迭代可以向后兼容,就别升版本折腾人了

7. 忽略API安全,基本裸奔就上线

这条是重中之重,放最后压轴说。

我见过太多接口:没有认证、没有限流、没有参数校验、没有防注入——就那么赤条条地上线了。然后:

  • 被爬虫一夜爬光所有数据
  • 被恶意用户高频请求打挂
  • 被SQL注入把数据库删了个干净
  • 被XSS攻击搞了个弹窗满天飞

必做的几件事:

认证和授权:JWT token、OAuth2、Session,随你选但必须有。没有认证的接口就像没锁的门,谁都能进。

限流(Rate Limiting):防止刷接口、防止DDoS。常用算法:固定窗口、滑动窗口、令牌桶、漏桶。简单场景用固定窗口就行:

# 响应头告诉前端还剩多少额度
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000  # 窗口重置时间戳

超限了返回429 Too Many Requests,别返回200然后在body里说"请求太频繁"——中间件看不懂。

参数校验:所有输入都是有毒的。后端必须校验数据类型、长度、格式、范围。别相信前端传过来的任何东西。

防注入:SQL注入、NoSQL注入、XSS、CSRF,能防的都防。用ORM框架的参数化查询,别自己拼SQL字符串。


写在最后

API设计这事,说难不难,说简单也不简单。核心就一句话:设计时多想一步,实现时少踩一坑

接口是给程序员用的,但更是给未来接手的人用的,也是给自己老了之后不想加班返工用的。写的时候多花10分钟设计,后面的维护成本可能省下的是10小时。

当然,最重要的还是——写完接口记得自测。一个自己都不愿意用的接口,就别指望测试会放过你了。

大家还有什么接口设计上的血泪史,欢迎评论区分享,我先去哭一会儿。

相关文章

当 AI 圈开始整活:那些让我眼前一亮(或者眼前一黑)的新玩意儿
API设计里那些没人告诉你的「潜规则」
RESTful API设计翻车实录:我用血泪经验换来的五条军规
API设计翻车现场:10个让我后悔莫及的蠢设计
凌晨三点,数据库:我超时了,但我不想告诉你为什么
别让API成为同事的噩梦:RESTful设计的血泪经验

发布评论