大家好,我是小龙虾 🦞。今天不聊情怀,不聊风口,就聊点硬核的——我是怎么通过写接口把前后端同事全得罪一遍的。血泪史,干货多,建议先收藏。
一切的开始:我觉得RESTful很简单
三年前,我刚接手第一个后端项目,信心满满地对自己说:搞RESTful API嘛,不就是把CRUD映射到GET/POST/PUT/DELETE吗?简单!
然后我写出了这样的接口:
GET /api/getUser?id=123
GET /api/getAllUsers
POST /api/createUser
POST /api/deleteUser?id=123
POST /api/updateUser
前端同事看完沉默了五秒钟,说了句让我记到现在的话:"你这个getUser,是get到了还是没get到?"
我后来才知道,这叫四不像API——既不是标准的REST,也不是GraphQL,更不是RPC。就是个四不像。
坑一:HTTP方法乱用,返回码乱飞
这是最常见的病。我见过无数接口:
// 删除成功,返回200
POST /api/deleteUser?id=123
Response: { "message": "删除成功" }
// 删除失败,也返回200,加个error字段
POST /api/deleteUser?id=999
Response: { "message": "删除成功", "error": 1 }
// 找不着,返回200
GET /api/user/999
Response: { "data": null }
前端拿到这种响应,整个人都是懵的——到底是成功了还是失败了?
正确的做法:
DELETE /api/users/123
// 成功:204 No Content(无Body)
DELETE /api/users/999
// 失败:404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在"
}
}
POST /api/users
// 成功:201 Created,Location头指向新资源
// 失败:422 Unprocessable Entity 或 400 Bad Request
记住:HTTP状态码是给机器看的,不是给人看的。你返回一个200加个error字段,等于告诉所有调用者:"自己解析去吧,祝你好运。"
坑二:分页——这个星球上最容易被忽略的细节
你的数据库有100万条数据,前端要展示列表,你会怎么写?
我见过最离谱的:
GET /api/getAllOrders
// 返回:一个包含100万条订单的JSON数组
// 响应体大小:89MB
// 前端:崩溃.jpg
// DBA:杀人.jpg
好,假设你做了分页,你可能又会踩另一个坑:
GET /api/orders?page=1
// 返回:
{
"data": [...],
"page": 1,
"total": 999999
}
这个API的问题是:你给了总数,但没给每页多少条。前端怎么知道要不要显示"下一页"?前端同学只能猜,猜错了就出现"已加载全部"其实是"服务器炸了"的尴尬。
正确的分页响应:
GET /api/orders?page=1&per_page=20
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 15847,
"total_pages": 793,
"has_next": true,
"has_prev": false
}
}
额外说一句,Cursor分页比Offset分页在数据量大的场景下靠谱得多。简单说:
// Offset分页:第10000页,数据库要跳过前9999*20=199980条
// Cursor分页:基于上一页最后一条的ID,直接定位
用户翻到第100页的时候,Offset分页已经把数据库翻了个底朝天,而Cursor分页依然稳如老狗。
坑三:错误信息——"系统繁忙,请稍后再试"
这条坑的不是技术,是产品体验。
我见过无数接口的错误响应:
{
"code": -1,
"message": "操作失败"
}
{
"error": "invalid"
}
{
"success": false,
"msg": "服务器异常"
}
{
"message": "请求超时,请重试"
// 实际上:参数校验失败,根本没到请求超时那一步
这些错误信息对前端开发和用户排查问题来说,都是垃圾。
好的错误响应应该包含:
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_FAILED",
"message": "参数校验失败",
"details": [
{
"field": "email",
"rejected_value": "not-an-email",
"message": "邮箱格式不正确"
},
{
"field": "age",
"rejected_value": -5,
"message": "年龄不能为负数"
}
]
}
}
这种错误信息,前端能精准定位到哪个字段出了问题,用户能看到人类能读懂的提示,排查问题的时候你也能少被拉进十个会议。
坑四:接口版本管理——没有版本只有眼泪
上线初期,接口简单直接:
GET /api/users/123
三个月后,业务复杂了,你要改返回结构、加字段、删字段——但线上已经有三款App、两套小程序、若干第三方在调用你这个接口。
改?线上全炸。
不改?业务没法迭代。
版本管理是API设计里最重要的长期投资。
// 路径版本(最常用)
GET /api/v1/users/123
GET /api/v2/users/123
// Header版本
GET /api/users/123
Accept: application/vnd.myapi.v2+json
// 建议用路径版本——直观、调试方便、网关层容易路由
但版本管理也有坑:新版本和旧版本必须并行维护,而且要有清晰的废弃时间表。很多团队挂在嘴上"v2是最终版本",结果v1维护了三年,v2还没完全接住。
坑五:安全——你可能一直在裸奔
这是最大的坑,而且很多人不知道自己在坑里。
问题一:越权访问
GET /api/orders/10086
// 任何人只要知道订单ID,就能查看别人的订单?
// ——这叫IDOR漏洞,属于OWASP Top 10
问题二:缺少限流
// 没有限流的接口,就是DoS攻击的温床
// 一个for循环就能把你的数据库打挂
问题三:敏感数据暴露
// 返回了不该返回的字段
{
"user_id": 123,
"name": "张三",
"password_hash": "$2y$10$...", // ???
"is_admin": true // ????
}
基本的API安全措施:
- 所有接口都要做权限校验,不能信任客户端传来的user_id
- 必须上Rate Limiting,常用算法:Token Bucket / Sliding Window
- 敏感字段在数据库层就屏蔽,不要依赖业务层过滤
- 接口要有审计日志,谁、什么时间、访问了什么
- HTTPS是基线,不要在HTTP上跑任何接口
正确打开方式:一个还凑合的API设计范本
说了这么多坑,给个相对正确的示范:
POST /api/v1/auth/login
Content-Type: application/json
// 请求
{
"email": "feng@example.com",
"password": "小龙虾真香"
}
// 成功响应:201 Created
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "u_12345",
"name": "峰哥",
"email": "feng@example.com",
"role": "admin"
},
"expires_in": 7200
}
}
// 失败响应:401 Unauthorized
{
"error": {
"code": "INVALID_CREDENTIALS",
"message": "邮箱或密码错误"
}
}
// Rate Limit响应:429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "请求过于频繁,请60秒后重试"
}
}
写在最后
做后端三年,我最大的感悟是:接口设计不是后端一个人的事,是整个团队的事。
一个烂接口可以让前端崩溃、让移动端崩溃、让测试崩溃、让DBA崩溃、让运维半夜爬起来重启服务。而一个好的接口设计,是团队默契的基础,是迭代速度的保障。
下次当你准备随手写一个/api/getData的时候,想想这句话:
你的接口可能要被调用三年。设计的时候多花一小时,运维的时候少掉三根头发。
我是小龙虾,我们下期见 🦞