API 设计的 7 个致命错误:为什么你的接口让前端想打人
凌晨一点,前端在群里 @了你
“后端大哥,为什么删除用户的接口是 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",// ISO 8601 标准格式"lastLoginAt":"2025-01-15T10:30:00Z","status":"active",// 语义化的状态"role":{"id":5,"name":"editor"}}核心原则:API 是给开发者用的,不是给数据库用的。
# Python/Django 示例:用序列化器隐藏实现细节from rest_framework import serializers classUserSerializer(serializers.ModelSerializer):# 重命名字段 display_name = serializers.CharField(source='user_name') created_at = serializers.DateTimeField(source='user_create_ts')# 自定义字段 status = serializers.SerializerMethodField()defget_status(self, obj):# 将数字状态码转换为语义化字符串return'active'if obj.user_status_flag ==1else'inactive'classMeta: 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:newDate(user.user_create_ts *1000).toISOString(),status: user.user_status_flag ===1?"active":"inactive",})})错误 2:HTTP 方法乱用,POST 包打天下
灾难现场
// ❌ 全部用 POSTPOST/ 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'])defget_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'])defget_user(user_id): user = User.query.get_or_404(user_id)return jsonify(user.to_dict())# 创建用户@app.route('/api/users', methods=['POST'])defcreate_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'])defupdate_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'])defpatch_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'])defdelete_user(user_id): user = User.query.get_or_404(user_id) db.session.delete(user) db.session.commit()return'',204# No Content特殊场景:非 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><head><title>500 Internal Server Error</title></head>...// 情况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 成功,但无返回内容(常用于 DELETE) └─ 206 Partial Content 部分内容(分页、断点续传) 4xx 客户端错误 ├─ 400 Bad Request 请求参数错误 ├─ 401 Unauthorized 未认证(需要登录) ├─ 403 Forbidden 无权限 ├─ 404 Not Found 资源不存在 ├─ 405 Method Not Allowed HTTP 方法不支持 ├─ 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__)# 自定义异常类classAPIError(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)defhandle_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)defhandle_not_found(error):return jsonify({'error':{'code':'RESOURCE_NOT_FOUND','message':'The requested resource was not found'}}),[email protected](500)defhandle_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'])defget_user(user_id): user = User.query.get(user_id)ifnot 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()// 自定义错误类classAPIErrorextendsError{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 instanceofAPIError){return res.status(err.statusCode).json({error:{code: err.code,message: err.message,details: err.details,},requestId:uuidv4(),timestamp:newDate().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:newDate().toISOString(),})})// 使用示例 app.get("/api/users/:id",async(req, res, next)=>{try{const user =await User.findById(req.params.id)if(!user){thrownewAPIError("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:没有版本控制,改接口全靠吼
灾难现场
// 第一版 APIGET/api/users/123{"name":"张三","age":25}// 三个月后,需求变了,直接改接口GET/api/users/123{"firstName":"三",// 改了字段名!"lastName":"张",// 拆分了字段!"birthDate":"1999-01-01"// age 改成 birthDate!}// 结果:所有老版本的 App 全部崩溃 💥为什么需要版本控制?
- 向后兼容:老版本的客户端不会因为 API 更新而崩溃
- 平滑迁移:给客户端足够的时间升级
- A/B 测试:可以同时运行多个版本
- 回滚方便:出问题可以快速回退
正确姿势:API 版本控制
方案 1:URL 路径版本(推荐)
// 最常用,最直观GET/ api / v1 / users /123GET/ api / v2 / users /123// 优点:// - 清晰明了,一眼就能看出版本// - 容易在路由层面做版本隔离// - 方便缓存和 CDN// 缺点:// - URL 会变化方案 2:请求头版本
// 通过 Accept 头指定版本GET/ api / users /123Accept: application / vnd.myapi.v1 + json GET/ api / users /123Accept: application / vnd.myapi.v2 + json // 优点:// - URL 保持不变// - 符合 HTTP 标准// 缺点:// - 不够直观// - 难以在浏览器中测试方案 3:查询参数版本
// 通过查询参数指定版本GET/api/users/123?version=1GET/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>')defget_user_v1(user_id): user = User.query.get_or_404(user_id)return jsonify({'name': user.name,# v1 返回完整姓名'age': user.age })@app.route('/api/v2/users/<int:user_id>')defget_user_v2(user_id): user = User.query.get_or_404(user_id)return jsonify({'firstName': user.first_name,# v2 拆分姓名'lastName': user.last_name,'birthDate': user.birth_date.isoformat()# v2 返回生日而不是年龄})# 方案2:请求头版本@app.route('/api/users/<int:user_id>')defget_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":[// 用户的所有文章!{"id":1,"title":"...","content":"..."},{"id":2,"title":"...","content":"..."},// ... 100 篇文章],"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"// IP 地址也不该返回!},// ... 更多用户]// 问题:// 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":"简短的个人简介..."// 截断到 100 字符}]// 详情视图:返回完整信息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__)deffilter_fields(data, fields):"""根据 fields 参数过滤数据"""ifnot fields:return data field_list = fields.split(',')ifisinstance(data,list):return[filter_fields(item, fields)for item in data]ifisinstance(data,dict): filtered ={}for field in field_list:if'.'in field:# 处理嵌套字段,如 profile.avatar parts = field.split('.',1) parent, child = parts[0], parts[1]if parent in data:if parent notin 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')defget_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()// 字段筛选中间件functionfieldFilter(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)}returnoriginalJson(data)}next()}functionfilterFields(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(".")]))}}elseif(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:分页、排序、过滤各自为政
灾难现场
// ❌ 每个接口的分页参数都不一样// 接口 AGET/api/users?page=1&pageSize=20// 接口 BGET/api/posts?pageNum=1&limit=20// 接口 CGET/api/comments?p=1&size=20&offset=0// 接口 DGET/api/orders?start=0&count=20// 前端开发者:我 TM...🤬为什么需要统一?
- 降低学习成本:前端不用记住每个接口的参数名
- 便于封装:可以写一个通用的分页组件
- 减少错误:统一的规范减少参数错误
- 提升体验:一致性让 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)defpaginate(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')defget_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()// 通用分页函数asyncfunctionpaginate(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)||1const limit =parseInt(req.query.limit)||20const search = req.query.search ||""const status = req.query.status ||""const sort = req.query.sort ||"id"const order = req.query.order ||"asc"// 构建 where 条件const where ={}if(search){ where.username ={[Op.like]:`%${search}%`}}if(status){ where.status = status }// 构建排序const orderBy =[[sort, order.toUpperCase()]]// 执行分页查询const result =awaitpaginate(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 设计的黄金法则
1. 以开发者为中心
API 是给开发者用的,不是给数据库用的。设计时要站在使用者的角度思考。
2. 保持一致性
命名规范、错误格式、分页参数…所有接口都应该遵循相同的规范。
3. 使用标准
HTTP 方法、状态码、日期格式…尽量使用业界标准,不要自己发明。
4. 文档先行
好的文档胜过千言万语。使用 OpenAPI/Swagger 自动生成文档。
5. 版本控制
从第一天就考虑版本控制,不要等到需要改接口时才想起来。
6. 性能优化
分页、字段筛选、缓存…从设计阶段就考虑性能问题。
7. 安全第一
认证、授权、限流、数据脱敏…安全永远是第一位的。
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:20responses:"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:200OK{"data":[...],"pagination":{"page":1,"limit":20,"total":100,"totalPages":5,"hasNext":true,"hasPrev":false}}// 获取单个用户GET/api/v1/users/123Response:200OK{"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:200OK{"id":123,"username":"zhangsan","email":"[email protected]","updatedAt":"2025-01-17T10:35:00Z"}// 删除用户DELETE/api/v1/users/123Response: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 吧!🚀