API 设计的 7 个致命错误:为什么你的接口让前端想打人

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}

问题在哪?

  1. 字段名丑陋user_pwd_hashuser_create_ts,这是给人看的还是给机器看的?
  2. 暴露实现细节:前端不需要知道你用的是时间戳还是日期字符串
  3. 难以演进:数据库改个字段名,API 就得跟着改,前端也得改
  4. 安全隐患:密码哈希值为什么要返回?

正确姿势

// ✅ 面向业务的 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 // 搜索用户

为什么这样不好?

  1. 违反 HTTP 语义:GET 请求应该是幂等的、可缓存的
  2. 无法利用浏览器缓存:POST 请求不会被缓存
  3. 无法使用 CDN:CDN 通常只缓存 GET 请求
  4. 难以调试:浏览器历史记录、书签都无法保存 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":"用户名或密码错误"// 国际化怎么办?}

为什么这样不好?

  1. HTTP 状态码失去意义:前端无法通过状态码判断请求是否成功
  2. 无法利用 HTTP 生态:拦截器、中间件、监控工具都依赖状态码
  3. 错误信息不够详细:前端不知道如何处理错误
  4. 难以调试:出问题时无法快速定位

正确姿势:标准化的错误响应

// ✅ 好的错误处理// 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/123ID 放在路径中
搜索过滤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 全部崩溃 💥

为什么需要版本控制?

  1. 向后兼容:老版本的客户端不会因为 API 更新而崩溃
  2. 平滑迁移:给客户端足够的时间升级
  3. A/B 测试:可以同时运行多个版本
  4. 回滚方便:出问题可以快速回退

正确姿势: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...🤬

为什么需要统一?

  1. 降低学习成本:前端不用记住每个接口的参数名
  2. 便于封装:可以写一个通用的分页组件
  3. 减少错误:统一的规范减少参数错误
  4. 提升体验:一致性让 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 设计工具

  1. 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"
  1. Postman
    • API 测试工具
    • 可以生成文档
    • 支持团队协作
  2. Insomnia
    • 轻量级 API 测试工具
    • 支持 GraphQL
    • 界面简洁

API 网关

  1. Kong
    • 开源 API 网关
    • 支持插件扩展
    • 性能优秀
  2. Nginx
    • 轻量级反向代理
    • 可以做限流、缓存
    • 配置灵活
  3. AWS API Gateway
    • 云原生 API 网关
    • 自动扩展
    • 与 AWS 服务集成

监控工具

  1. Sentry
    • 错误追踪
    • 性能监控
    • 支持多种语言
  2. Datadog
    • APM 监控
    • 日志聚合
    • 实时告警
  3. New Relic
    • 应用性能监控
    • 分布式追踪
    • 用户体验监控

写在最后:好的 API 是产品的一部分

很多人觉得 API 只是技术实现,随便写写就行。

但实际上,API 是你产品的一部分。

一个设计良好的 API:

  • 让前端开发效率提升 50%
  • 让 Bug 数量减少 70%
  • 让新人上手时间缩短 80%
  • 让产品迭代速度加快 2 倍

而一个设计糟糕的 API:

  • 让前端天天骂娘
  • 让 Bug 层出不穷
  • 让新人一脸懵逼
  • 让产品迭代举步维艰

2026 年了,别再写让人想打人的 API 了。

记住这 7 个致命错误:

  1. ❌ 暴露数据库结构
  2. ❌ HTTP 方法乱用
  3. ❌ 错误处理混乱
  4. ❌ URL 设计混乱
  5. ❌ 没有版本控制
  6. ❌ 返回数据过多
  7. ❌ 查询参数不统一

避开这些坑,你的 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 吧!🚀

Read more

C++分布式语音识别服务实践

C++分布式语音识别服务实践

基于 brpc+etcd + 百度 AI SDK 的分布式语音识别服务实践:从代码架构到踩坑复盘 一、项目背景与核心功能 最近基于 C++ 实现了一个分布式语音识别子服务,核心目标是提供高可用的 RPC 接口,支持客户端上传 PCM 音频文件并返回识别结果。技术栈选型如下: * RPC 框架:brpc(百度开源高性能 RPC 框架,支持多种协议); * 数据序列化:Protobuf(定义 RPC 接口和数据结构); * 服务注册与发现:etcd(分布式键值存储,实现服务上下线感知); * 语音识别能力:百度 AI 语音 SDK(提供成熟的 PCM 音频转文字能力); * 日志与配置:spdlog(高性能日志库)、gflags(命令行参数解析)。 项目分为服务端和客户端两部分:

By Ne0inhk
C++ 抽象类与多态原理深度解析:从纯虚函数到虚表机制(附高频面试题)

C++ 抽象类与多态原理深度解析:从纯虚函数到虚表机制(附高频面试题)

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 纯虚函数与抽象类:强制接口规范的“契约” * 1.1 纯虚函数:没有实现的 “接口声明” * 1.2 抽象类:包含纯虚函数的 “不可实例化类” * 二. 多态的底层原理:虚表指针与虚函数表 * 2.1 虚表指针(vfptr):对象中的 “导航器” * 2.2 多态的实现原理 * 2.3 虚函数表(vtable):存储虚函数地址的 “数组” * 2.4 动态绑定与静态绑定 * 三. 关键问题辨析与总结

By Ne0inhk
《C++ 递归、搜索与回溯》第1题:汉诺塔问题

《C++ 递归、搜索与回溯》第1题:汉诺塔问题

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 《Git深度解析》:版本管理实战全解 🌟心向往之行必能至 🎥Cx330🌸的简介: 前言: 聚焦算法题实战,系统讲解三大核心板块:“精准定位最优解”——优选算法,“简化逻辑表达,系统性探索与剪枝优化”——递归与回溯,“以局部最优换全局高效”——贪心算法,讲解思路与代码实现,帮助大家快速提升代码能力 目录 前言: 递归,搜索与回溯算法前置知识 1. 汉诺塔 算法原理(递归): 思路: 算法流程: 解法代码(C++): 博主手记(字体还请见谅哈): 结尾: 递归,搜索与回溯算法前置知识 1. 汉诺塔 题目链接: 面试题 08.

By Ne0inhk
【C++藏宝阁】C++入门:命名空间(namespace)详解

【C++藏宝阁】C++入门:命名空间(namespace)详解

🌈个人主页:聆风吟 🔥系列专栏:C++藏宝阁 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 * 📚专栏订阅推荐 * 📋前言:为什么需要命名空间? * 一、命名空间的定义 * 二、命名空间的使用 * 三、命名空间的特性 * 3.1 命名空间的嵌套定义 * 3.2 命名空间的定义可以不连续 * 四、命名空间的本质:独立的作用域 * 4.1 命名空间是C++的一种作用域类型 * 4.2 命名空间作用域的特点 * 4.3 域作用限定符 `::` 的作用 * 4.4 编译器的查找规则 * 五、命名空间的价值 * 5.1 解决命名冲突 * 5.2 模块化组织代码 * 5.3

By Ne0inhk