凌晨一点,前端在群里 @了你
'后端大哥,为什么删除用户的接口是 POST?' '为什么获取用户列表要传 20 个参数?' '为什么同一个错误,有时返回 200,有时返回 500?' '能不能别再改接口了?这是这个月第三次了!'
你看着手机,心里一万头草泥马奔腾而过。
明明功能都实现了,为什么前端还是不满意?
因为你的 API 设计,可能犯了这 7 个致命错误。
今天,我们就来聊聊那些让前端抓狂、让自己背锅、让项目延期的 API 设计问题。
错误 1:把数据库表结构直接暴露成 API
问题示例
# ❌ 直接暴露数据库结构
GET /api/user_account_info?user_id=123
# 返回
{
"user_id": 123,
"user_name": "zhangsan",
"user_pwd_hash": "5f4dcc3b5aa765d61d8327deb882cf99",
"user_create_ts": 1704067200,
"user_last_login_ts": 1736899200,
"user_status_flag": 1,
"user_role_id": 5,
"user_dept_id": 10
}
问题在哪?
- 字段名丑陋:
user_pwd_hash、user_create_ts,这是给人看的还是给机器看的? - 暴露实现细节:前端不需要知道你用的是时间戳还是日期字符串
- 难以演进:数据库改个字段名,API 就得跟着改,前端也得改
- 安全隐患:密码哈希值为什么要返回?
规范示例
// ✅ 面向业务的 API 设计
GET /api/users/123
// 返回
{
"id": 123,
"username": "zhangsan",
"displayName": "张三",
"createdAt": "2024-01-01T00:00:00Z",
"lastLoginAt": "2025-01-15T10:30:00Z",
"status": "active",
"role": {"id": 5, "name": "editor"}
}
核心原则:API 是给开发者用的,不是给数据库用的。
# Python/Django 示例:用序列化器隐藏实现细节
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
# 重命名字段
display_name = serializers.CharField(source='user_name')
created_at = serializers.DateTimeField(source='user_create_ts')
# 自定义字段
status = serializers.SerializerMethodField()
def get_status(self, obj):
# 将数字状态码转换为语义化字符串
return 'active' if obj.user_status_flag == 1 else 'inactive'
class Meta:
model = User
fields = ['id', 'username', 'display_name', 'created_at', 'status']
exclude = ['user_pwd_hash']
// Node.js/Express 示例
app.get("/api/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
// 转换数据格式
res.json({
id: user.user_id,
username: user.user_name,
displayName: user.user_name,
createdAt: new Date(user.user_create_ts * 1000).toISOString(),
status: user.user_status_flag === 1 ? "active" : "inactive"
});
});
错误 2:HTTP 方法乱用,POST 包打天下
问题示例
// ❌ 全部用 POST
POST /api/getUser // 获取用户
POST /api/createUser // 创建用户
POST /api/updateUser // 更新用户
POST /api/deleteUser // 删除用户
POST /api/searchUsers // 搜索用户
为什么这样不好?
- 违反 HTTP 语义:GET 请求应该是幂等的、可缓存的
- 无法利用浏览器缓存:POST 请求不会被缓存
- 无法使用 CDN:CDN 通常只缓存 GET 请求
- 难以调试:浏览器历史记录、书签都无法保存 POST 请求
规范示例:RESTful 风格
// ✅ 正确使用 HTTP 方法
GET /api/users // 获取用户列表
GET /api/users/123 // 获取单个用户
POST /api/users // 创建用户
PUT /api/users/123 // 完整更新用户
PATCH /api/users/123 // 部分更新用户
DELETE /api/users/123 // 删除用户
HTTP 方法速查表:
| 方法 | 用途 | 幂等性 | 安全性 |
|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ |
| POST | 创建资源 | ❌ | ❌ |
| PUT | 完整更新 | ✅ | ❌ |
| PATCH | 部分更新 | ❌ | ❌ |
| DELETE | 删除资源 | ✅ | ❌ |
幂等性:多次执行结果相同 安全性:不会修改服务器状态
实战代码
# Python/Flask 示例
from flask import Flask, request, jsonify
app = Flask(__name__)
# 获取用户列表
@app.route('/api/users', methods=['GET'])
def get_users():
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
users = User.query.paginate(page=page, per_page=limit)
return jsonify([user.to_dict() for user in users.items])
# 获取单个用户
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# 创建用户
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
user = User(username=data['username'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
# 完整更新用户
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.username = data['username']
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
# 部分更新用户
@app.route('/api/users/<int:user_id>', methods=['PATCH'])
def patch_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'username' in data: user.username = data['username']
if 'email' in data: user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
# 删除用户
@app.route('/api/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return '', 204
特殊场景:非 CRUD 操作怎么办?
有些业务操作不是简单的增删改查,比如发送邮件、重置密码、取消订单、点赞文章。
方案 1:把操作建模成资源
// ❌ 不好:用动词
POST /api/sendEmail
POST /api/resetPassword
POST /api/cancelOrder
// ✅ 好:把操作建模成资源
POST /api/emails // 创建一封邮件(发送)
POST /api/password-resets // 创建一个密码重置请求
POST /api/orders/123/cancellation // 创建一个取消订单的操作
方案 2:使用子资源
// 点赞文章
POST /api/posts/123/likes // 点赞
DELETE /api/posts/123/likes // 取消点赞
// 关注用户
POST /api/users/123/followers // 关注
DELETE /api/users/123/followers // 取消关注
方案 3:使用动作端点(最后的选择)
// 如果实在无法建模成资源,可以使用动作端点
POST /api/users/123/actions/activate // 激活用户
POST /api/orders/123/actions/refund // 退款
POST /api/posts/123/actions/publish // 发布文章
错误 3:错误处理一团糟
问题示例
// ❌ 错误处理的反面教材
// 情况 1:成功和失败都返回 200
{"code": 0, "message": "success", "data": {...}}
{"code": 1001, "message": "用户不存在", "data": null}
// 情况 2:错误信息不明确
{"error": "error"} // 这是什么错误?
// 情况 3:返回 HTML 错误页面
<!DOCTYPE html><html>...</html>
// 情况 4:错误信息只有中文
{"error": "用户名或密码错误"} // 国际化怎么办?
为什么这样不好?
- HTTP 状态码失去意义:前端无法通过状态码判断请求是否成功
- 无法利用 HTTP 生态:拦截器、中间件、监控工具都依赖状态码
- 错误信息不够详细:前端不知道如何处理错误
- 难以调试:出问题时无法快速定位
规范示例:标准化的错误响应
// ✅ 好的错误处理
// 1. 使用正确的 HTTP 状态码
// 400 Bad Request - 客户端错误
{
"error": {
"code": "INVALID_INPUT",
"message": "Validation failed",
"details": [
{"field": "email", "message": "Email format is invalid"},
{"field": "age", "message": "Age must be greater than 0"}
]
},
"requestId": "req_abc123",
"timestamp": "2025-01-17T10:30:00Z"
}
// 401 Unauthorized - 未认证
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required",
"details": "Please provide a valid access token"
}
}
// 403 Forbidden - 无权限
{
"error": {
"code": "FORBIDDEN",
"message": "Insufficient permissions",
"details": "You need 'admin' role to perform this action"
}
}
// 404 Not Found - 资源不存在
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User not found",
"details": "User with ID 123 does not exist"
}
}
// 429 Too Many Requests - 限流
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"details": "Rate limit: 100 requests per minute",
"retryAfter": 45
}
}
// 500 Internal Server Error - 服务器错误
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"details": "Please contact support if the problem persists",
"requestId": "req_abc123"
}
}
HTTP 状态码速查表
- 2xx 成功:200 OK, 201 Created, 202 Accepted, 204 No Content, 206 Partial Content
- 4xx 客户端错误:400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
- 5xx 服务器错误:500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
实战代码:统一错误处理
# Python/Flask 示例
from flask import Flask, jsonify
from datetime import datetime
import uuid
app = Flask(__name__)
# 自定义异常类
class APIError(Exception):
def __init__(self, code, message, details=None, status_code=400):
self.code = code
self.message = message
self.details = details
self.status_code = status_code
# 全局错误处理器
@app.errorhandler(APIError)
def handle_api_error(error):
response = {
'error': {
'code': error.code,
'message': error.message,
'details': error.details
},
'requestId': str(uuid.uuid4()),
'timestamp': datetime.utcnow().isoformat() + 'Z'
}
return jsonify(response), error.status_code
@app.errorhandler(404)
def handle_not_found(error):
return jsonify({'error': {'code': 'RESOURCE_NOT_FOUND', 'message': 'The requested resource was not found'}}), 404
@app.errorhandler(500)
def handle_internal_error(error):
app.logger.error(f'Internal error: {error}')
return jsonify({
'error': {
'code': 'INTERNAL_SERVER_ERROR',
'message': 'An unexpected error occurred',
'details': 'Please contact support if the problem persists'
},
'requestId': str(uuid.uuid4())
}), 500
# 使用示例
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get(user_id)
if not user:
raise APIError(
code='USER_NOT_FOUND',
message='User not found',
details=f'User with ID {user_id} does not exist',
status_code=404
)
return jsonify(user.to_dict())
// Node.js/Express 示例
const express = require("express");
const { v4: uuidv4 } = require("uuid");
const app = express();
// 自定义错误类
class APIError extends Error {
constructor(code, message, details, statusCode = 400) {
super(message);
this.code = code;
this.details = details;
this.statusCode = statusCode;
}
}
// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error("Error:", err);
if (err instanceof APIError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details
},
requestId: uuidv4(),
timestamp: new Date().toISOString()
});
}
res.status(500).json({
error: {
code: "INTERNAL_SERVER_ERROR",
message: "An unexpected error occurred",
details: process.env.NODE_ENV === "development" ? err.message : undefined
},
requestId: uuidv4(),
timestamp: new Date().toISOString()
});
});
// 使用示例
app.get("/api/users/:id", async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new APIError("USER_NOT_FOUND", "User not found", `User with ID ${req.params.id} does not exist`, 404);
}
res.json(user);
} catch (error) {
next(error);
}
});
错误 4:URL 设计混乱不堪
问题示例
// ❌ 各种混乱的 URL 设计
// 问题 1:动词 + 名词混用
GET /api/getUsers
GET /api/user/list
GET /api/fetchUserData
// 问题 2:过度嵌套
GET /api/v1/company/123/department/456/team/789/user/111/posts/222/comments/333
// 问题 3:命名不一致
GET /api/users // 复数
GET /api/product // 单数
GET /api/order-list // 短横线
GET /api/userProfile // 驼峰
// 问题 4:查询参数放在路径里
GET /api/users/search/name/zhangsan/age/25
规范示例:清晰的 URL 设计
// ✅ 好的 URL 设计原则
// 1. 使用名词,不用动词
GET /api/users // ✅
GET /api/getUsers // ❌
// 2. 使用复数形式
GET /api/users // ✅
GET /api/user // ❌
// 3. 使用短横线分隔单词
GET /api/user-profiles // ✅
GET /api/userProfiles // ❌
GET /api/user_profiles // ❌
// 4. 资源嵌套不超过 2 层
GET /api/users/123/posts // ✅
GET /api/posts?userId=123 // ✅
GET /api/users/123/posts/456/comments // ❌ 太深了
// 5. 查询、过滤、排序用查询参数
GET /api/users?name=zhangsan&age=25 // ✅
GET /api/users/search/name/zhangsan // ❌
// 6. 分页用查询参数
GET /api/users?page=1&limit=20 // ✅
GET /api/users/page/1/limit/20 // ❌
URL 设计速查表
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 获取列表 | GET /api/users | 使用复数名词 |
| 获取单个 | GET /api/users/123 | ID 放在路径中 |
| 搜索过滤 | GET /api/users?name=zhang&age=25 | 使用查询参数 |
| 分页 | GET /api/users?page=1&limit=20 | 使用查询参数 |
| 排序 | GET /api/users?sort=createdAt&order=desc | 使用查询参数 |
| 关联资源 | GET /api/users/123/posts | 嵌套不超过 2 层 |
| 字段筛选 | GET /api/users?fields=id,name,email | 使用查询参数 |
| 版本控制 | GET /api/v1/users | 版本号放在路径开头 |
实战示例:完整的用户 API
// 用户管理
GET /api/v1/users // 获取用户列表
GET /api/v1/users/123 // 获取单个用户
POST /api/v1/users // 创建用户
PUT /api/v1/users/123 // 更新用户
PATCH /api/v1/users/123 // 部分更新用户
DELETE /api/v1/users/123 // 删除用户
// 用户的文章
GET /api/v1/users/123/posts // 获取用户的文章列表
POST /api/v1/users/123/posts // 为用户创建文章
// 用户的关注者
GET /api/v1/users/123/followers // 获取关注者列表
POST /api/v1/users/123/followers // 关注用户
DELETE /api/v1/users/123/followers/456 // 取消关注
// 搜索和过滤
GET /api/v1/users?name=zhang // 按名字搜索
GET /api/v1/users?age=25&city=beijing // 多条件过滤
GET /api/v1/users?status=active // 按状态过滤
// 分页和排序
GET /api/v1/users?page=1&limit=20 // 分页
GET /api/v1/users?sort=createdAt&order=desc // 排序
// 字段筛选(减少响应体积)
GET /api/v1/users?fields=id,name,email // 只返回指定字段
错误 5:没有版本控制,改接口全靠吼
问题示例
// 第一版 API
GET /api/users/123
{"name":"张三","age":25}
// 三个月后,需求变了,直接改接口
GET /api/users/123
{"firstName":"三","lastName":"张","birthDate":"1999-01-01"}
// 结果:所有老版本的 App 全部崩溃 💥
为什么需要版本控制?
- 向后兼容:老版本的客户端不会因为 API 更新而崩溃
- 平滑迁移:给客户端足够的时间升级
- A/B 测试:可以同时运行多个版本
- 回滚方便:出问题可以快速回退
规范示例:API 版本控制
方案 1:URL 路径版本(推荐)
// 最常用,最直观
GET /api/v1/users/123
GET /api/v2/users/123
// 优点:
// - 清晰明了,一眼就能看出版本
// - 容易在路由层面做版本隔离
// - 方便缓存和 CDN
// 缺点:
// - URL 会变化
方案 2:请求头版本
// 通过 Accept 头指定版本
GET /api/users/123
Accept: application/vnd.myapi.v1+json
GET /api/users/123
Accept: application/vnd.myapi.v2+json
// 优点:
// - URL 保持不变
// - 符合 HTTP 标准
// 缺点:
// - 不够直观
// - 难以在浏览器中测试
方案 3:查询参数版本
// 通过查询参数指定版本
GET /api/users/123?version=1
GET /api/users/123?version=2
// 优点:
// - 简单易用
// - 容易测试
// 缺点:
// - 容易被忽略
// - 影响缓存
实战代码:版本控制实现
# Python/Flask 示例
from flask import Flask, jsonify, request
app = Flask(__name__)
# 方案 1:URL 路径版本
@app.route('/api/v1/users/<int:user_id>')
def get_user_v1(user_id):
user = User.query.get_or_404(user_id)
return jsonify({'name': user.name, 'age': user.age})
@app.route('/api/v2/users/<int:user_id>')
def get_user_v2(user_id):
user = User.query.get_or_404(user_id)
return jsonify({
'firstName': user.first_name,
'lastName': user.last_name,
'birthDate': user.birth_date.isoformat()
})
# 方案 2:请求头版本
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
version = request.headers.get('API-Version', 'v1')
user = User.query.get_or_404(user_id)
if version == 'v1':
return jsonify({'name': user.name, 'age': user.age})
elif version == 'v2':
return jsonify({
'firstName': user.first_name,
'lastName': user.last_name,
'birthDate': user.birth_date.isoformat()
})
else:
return jsonify({'error': 'Unsupported API version'}), 400
// Node.js/Express 示例
const express = require("express");
const app = express();
// 创建版本路由
const v1Router = express.Router();
const v2Router = express.Router();
// V1 API
v1Router.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
res.json({ name: user.name, age: user.age });
});
// V2 API
v2Router.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
res.json({ firstName: user.firstName, lastName: user.lastName, birthDate: user.birthDate });
});
// 挂载路由
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);
// 默认版本(可选)
app.use("/api", v2Router); // 默认使用最新版本
版本废弃策略
// 在响应头中标记废弃信息
app.use('/api/v1', (req, res, next) => {
res.set({
'X-API-Deprecated': 'true',
'X-API-Deprecation-Date': '2025-12-31',
'X-API-Sunset-Date': '2026-03-31',
'Link': '</api/v2>; rel="successor-version"'
});
next();
});
// 在响应体中也可以包含废弃信息
{
"data": {...},
"meta": {
"deprecated": true,
"deprecationDate": "2025-12-31",
"sunsetDate": "2026-03-31",
"message": "This API version will be sunset on 2026-03-31. Please migrate to v2.",
"migrationGuide": "https://docs.example.com/api/v1-to-v2-migration"
}
}
错误 6:返回数据一股脑全给,前端要啥给啥
问题示例
// ❌ 获取用户列表,返回了一堆不需要的数据
GET /api/users
[
{
"id": 1,
"username": "zhangsan",
"email": "[email protected]",
"passwordHash": "5f4dcc3b5aa765d61d8327deb882cf99",
"phoneNumber": "13800138000",
"address": "北京市朝阳区...",
"bio": "这是一段很长的个人简介...",
"settings": {"theme": "dark", "language": "zh-CN", "notifications": {...}},
"posts": [...],
"followers": [...],
"following": [...],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2025-01-15T10:30:00Z",
"lastLoginAt": "2025-01-17T08:00:00Z",
"loginCount": 1234,
"ipAddress": "192.168.1.1"
}
]
// 问题:
// 1. 响应体积巨大(可能几 MB)
// 2. 包含敏感信息
// 3. 前端只需要 id、username、email,其他都是浪费
// 4. 加载慢,用户体验差
规范示例:按需返回数据
方案 1:字段筛选(Sparse Fieldsets)
// 只返回需要的字段
GET /api/users?fields=id,username,email
[
{"id": 1, "username": "zhangsan", "email": "[email protected]"},
{"id": 2, "username": "lisi", "email": "[email protected]"}
]
// 支持嵌套字段
GET /api/users?fields=id,username,profile.avatar,profile.bio
[
{"id": 1, "username": "zhangsan", "profile": {"avatar": "https://...", "bio": "..."}}
]
方案 2:不同场景返回不同数据
// 列表视图:只返回摘要信息
GET /api/users
[{
"id": 1,
"username": "zhangsan",
"avatar": "https://...",
"bio": "简短的个人简介..."
}]
// 详情视图:返回完整信息
GET /api/users/1
{
"id": 1,
"username": "zhangsan",
"email": "[email protected]",
"avatar": "https://...",
"bio": "完整的个人简介...",
"profile": {"location": "北京", "website": "https://...", "joinedAt": "2024-01-01T00:00:00Z"},
"stats": {"postsCount": 42, "followersCount": 1234, "followingCount": 567}
}
方案 3:使用 GraphQL(终极方案)
# 前端精确指定需要的字段
query { users { id username email } }
# 需要更多信息时
query { users { id username email profile { avatar bio } posts(limit: 5) { id title } } }
实战代码:字段筛选实现
# Python/Flask 示例
from flask import Flask, request, jsonify
app = Flask(__name__)
def filter_fields(data, fields):
if not fields: return data
field_list = fields.split(',')
if isinstance(data, list):
return [filter_fields(item, fields) for item in data]
if isinstance(data, dict):
filtered = {}
for field in field_list:
if '.' in field:
parts = field.split('.', 1)
parent, child = parts[0], parts[1]
if parent in data:
if parent not in filtered: filtered[parent] = {}
filtered[parent].update(filter_fields(data[parent], child))
elif field in data:
filtered[field] = data[field]
return filtered
return data
@app.route('/api/users')
def get_users():
users = User.query.all()
data = [user.to_dict() for user in users]
fields = request.args.get('fields')
if fields:
data = filter_fields(data, fields)
return jsonify(data)
// Node.js/Express 示例
const express = require("express");
const app = express();
function fieldFilter(req, res, next) {
const originalJson = res.json.bind(res);
res.json = function(data) {
const fields = req.query.fields;
if (fields) {
const fieldList = fields.split(",");
data = filterFields(data, fieldList);
}
return originalJson(data);
};
next();
}
function filterFields(data, fields) {
if (Array.isArray(data)) {
return data.map((item) => filterFields(item, fields));
}
if (typeof data === "object" && data !== null) {
const filtered = {};
fields.forEach((field) => {
if (field.includes(".")) {
const [parent, ...rest] = field.split(".");
if (data[parent]) {
filtered[parent] = filtered[parent] || {};
Object.assign(filtered[parent], filterFields(data[parent], [rest.join(".")]));
}
} else if (field in data) {
filtered[field] = data[field];
}
});
return filtered;
}
return data;
}
app.use(fieldFilter);
app.get("/api/users", async (req, res) => {
const users = await User.findAll();
res.json(users);
});
错误 7:分页、排序、过滤各自为政
问题示例
// ❌ 每个接口的分页参数都不一样
// 接口 A
GET /api/users?page=1&pageSize=20
// 接口 B
GET /api/posts?pageNum=1&limit=20
// 接口 C
GET /api/comments?p=1&size=20&offset=0
// 接口 D
GET /api/orders?start=0&count=20
为什么需要统一?
- 降低学习成本:前端不用记住每个接口的参数名
- 便于封装:可以写一个通用的分页组件
- 减少错误:统一的规范减少参数错误
- 提升体验:一致性让 API 更专业
规范示例:统一的查询规范
// ✅ 统一的分页、排序、过滤规范
// 分页参数
GET /api/users?page=1&limit=20
// 排序参数
GET /api/users?sort=createdAt&order=desc
GET /api/users?sort=-createdAt // 也可以用负号表示降序
// 过滤参数
GET /api/users?status=active&role=admin
GET /api/users?age[gte]=18&age[lte]=60 // 范围查询
// 搜索参数
GET /api/users?search=zhang
// 组合使用
GET /api/users?page=1&limit=20&sort=-createdAt&status=active&search=zhang
标准响应格式
// ✅ 统一的分页响应格式
{
"data": [{"id": 1, "username": "zhangsan"}, {"id": 2, "username": "lisi"}],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
},
"links": {
"self": "/api/users?page=1&limit=20",
"first": "/api/users?page=1&limit=20",
"last": "/api/users?page=5&limit=20",
"next": "/api/users?page=2&limit=20",
"prev": null
}
}
实战代码:通用分页实现
# Python/Flask 示例
from flask import Flask, request, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
def paginate(query, page=1, limit=20):
total = query.count()
total_pages = (total + limit - 1) // limit
items = query.offset((page - 1) * limit).limit(limit).all()
return {
'data': [item.to_dict() for item in items],
'pagination': {
'page': page,
'limit': limit,
'total': total,
'totalPages': total_pages,
'hasNext': page < total_pages,
'hasPrev': page > 1
}
}
@app.route('/api/users')
def get_users():
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
sort = request.args.get('sort', 'id')
order = request.args.get('order', 'asc')
search = request.args.get('search', '')
status = request.args.get('status', '')
query = User.query
if search:
query = query.filter(User.username.like(f'%{search}%'))
if status:
query = query.filter(User.status == status)
if order == 'desc':
query = query.order_by(getattr(User, sort).desc())
else:
query = query.order_by(getattr(User, sort).asc())
result = paginate(query, page, limit)
return jsonify(result)
// Node.js/Express + Sequelize 示例
const express = require("express");
const { Op } = require("sequelize");
const app = express();
async function paginate(model, options = {}) {
const { page = 1, limit = 20, where = {}, order = [["id", "ASC"]], attributes } = options;
const offset = (page - 1) * limit;
const { count, rows } = await model.findAndCountAll({ where, order, limit, offset, attributes });
const totalPages = Math.ceil(count / limit);
return {
data: rows,
pagination: {
page,
limit,
total: count,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
};
}
app.get("/api/users", async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const search = req.query.search || "";
const status = req.query.status || "";
const sort = req.query.sort || "id";
const order = req.query.order || "asc";
const where = {};
if (search) where.username = { [Op.like]: `%${search}%` };
if (status) where.status = status;
const orderBy = [[sort, order.toUpperCase()]];
const result = await paginate(User, { page, limit, where, order: orderBy });
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
高级过滤语法
// 支持复杂的过滤条件
// 等于
GET /api/users?status=active
// 不等于
GET /api/users?status[ne]=inactive
// 大于/小于
GET /api/users?age[gt]=18&age[lt]=60
// 大于等于/小于等于
GET /api/users?age[gte]=18&age[lte]=60
// 包含(数组)
GET /api/users?role[in]=admin,editor
// 不包含
GET /api/users?role[nin]=guest
// 模糊搜索
GET /api/users?username[like]=zhang
// 日期范围
GET /api/users?createdAt[gte]=2024-01-01&createdAt[lte]=2024-12-31
// 多个条件组合
GET /api/users?status=active&age[gte]=18&role[in]=admin,editor&sort=-createdAt
总结:API 设计的黄金法则
- 以开发者为中心:API 是给开发者用的,不是给数据库用的。设计时要站在使用者的角度思考。
- 保持一致性:命名规范、错误格式、分页参数…所有接口都应该遵循相同的规范。
- 使用标准:HTTP 方法、状态码、日期格式…尽量使用业界标准,不要自己发明。
- 文档先行:好的文档胜过千言万语。使用 OpenAPI/Swagger 自动生成文档。
- 版本控制:从第一天就考虑版本控制,不要等到需要改接口时才想起来。
- 性能优化:分页、字段筛选、缓存…从设计阶段就考虑性能问题。
- 安全第一:认证、授权、限流、数据脱敏…安全永远是第一位的。
API 设计检查清单
在发布 API 之前,问自己这些问题:
基础设计
- URL 使用名词而不是动词?
- 使用了正确的 HTTP 方法?
- 命名规范统一(全部用短横线或驼峰)?
- 资源嵌套不超过 2 层?
数据格式
- 使用 JSON 作为默认格式?
- 日期使用 ISO 8601 格式?
- 字段名使用驼峰命名?
- 没有暴露数据库实现细节?
错误处理
- 使用了正确的 HTTP 状态码?
- 错误响应包含 code、message、details?
- 提供了 requestId 用于追踪?
- 错误信息对开发者友好?
版本控制
- 有明确的版本号(v1、v2)?
- 有版本废弃策略?
- 文档中说明了版本差异?
性能优化
- 支持分页?
- 支持字段筛选?
- 支持排序和过滤?
- 考虑了缓存策略?
安全性
- 使用 HTTPS?
- 有认证机制?
- 有权限控制?
- 有限流保护?
- 敏感数据已脱敏?
文档
- 有完整的 API 文档?
- 有请求/响应示例?
- 有错误码说明?
- 有变更日志?
推荐工具
API 设计工具
- Swagger/OpenAPI
- 自动生成 API 文档
- 支持在线测试
- 可以生成客户端代码
# openapi.yaml 示例
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/api/v1/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
"200":
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: $ref: "#/components/schemas/User"
pagination: $ref: "#/components/schemas/Pagination"
- Postman
- API 测试工具
- 可以生成文档
- 支持团队协作
- Insomnia
- 轻量级 API 测试工具
- 支持 GraphQL
- 界面简洁
API 网关
- Kong
- 开源 API 网关
- 支持插件扩展
- 性能优秀
- Nginx
- 轻量级反向代理
- 可以做限流、缓存
- 配置灵活
- AWS API Gateway
- 云原生 API 网关
- 自动扩展
- 与 AWS 服务集成
监控工具
- Sentry
- 错误追踪
- 性能监控
- 支持多种语言
- Datadog
- APM 监控
- 日志聚合
- 实时告警
- New Relic
- 应用性能监控
- 分布式追踪
- 用户体验监控
写在最后:好的 API 是产品的一部分
很多人觉得 API 只是技术实现,随便写写就行。
但实际上,API 是你产品的一部分。
一个设计良好的 API:
- 让前端开发效率提升 50%
- 让 Bug 数量减少 70%
- 让新人上手时间缩短 80%
- 让产品迭代速度加快 2 倍
而一个设计糟糕的 API:
- 让前端天天骂娘
- 让 Bug 层出不穷
- 让新人一脸懵逼
- 让产品迭代举步维艰
2026 年了,别再写让人想打人的 API 了。
记住这 7 个致命错误:
- ❌ 暴露数据库结构
- ❌ HTTP 方法乱用
- ❌ 错误处理混乱
- ❌ URL 设计混乱
- ❌ 没有版本控制
- ❌ 返回数据过多
- ❌ 查询参数不统一
避开这些坑,你的 API 就能让前端竖起大拇指。
下次再有前端在群里 @你,希望是来夸你的。
彩蛋:一个完整的 API 设计示例
// 用户管理 API - 完整示例
// ============ 基础 CRUD ============
// 获取用户列表(支持分页、搜索、过滤、排序)
GET /api/v1/users?page=1&limit=20&search=zhang&status=active&sort=-createdAt
Response: 200 OK
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}
// 获取单个用户
GET /api/v1/users/123
Response: 200 OK
{
"id": 123,
"username": "zhangsan",
"email": "[email protected]",
"profile": {...},
"createdAt": "2024-01-01T00:00:00Z"
}
// 创建用户
POST /api/v1/users
Request Body: {"username": "zhangsan", "email": "[email protected]", "password": "********"}
Response: 201 Created
{
"id": 123,
"username": "zhangsan",
"email": "[email protected]",
"createdAt": "2025-01-17T10:30:00Z"
}
// 更新用户
PATCH /api/v1/users/123
Request Body: {"email": "[email protected]"}
Response: 200 OK
{
"id": 123,
"username": "zhangsan",
"email": "[email protected]",
"updatedAt": "2025-01-17T10:35:00Z"
}
// 删除用户
DELETE /api/v1/users/123
Response: 204 No Content
// ============ 关联资源 ============
// 获取用户的文章
GET /api/v1/users/123/posts?page=1&limit=10
// 获取用户的关注者
GET /api/v1/users/123/followers
// 关注用户
POST /api/v1/users/123/followers
Request Body: {"userId": 456}
// 取消关注
DELETE /api/v1/users/123/followers/456
// ============ 错误响应 ============
// 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [{"field": "email", "message": "Email format is invalid"}]
},
"requestId": "req_abc123",
"timestamp": "2025-01-17T10:30:00Z"
}
// 404 Not Found
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": "User with ID 123 does not exist"
},
"requestId": "req_abc123",
"timestamp": "2025-01-17T10:30:00Z"
}
现在,去设计一个让人赞不绝口的 API 吧!🚀


