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

【算法通关指南:算法基础篇】 二维前缀和专题: 1. 【模板】二维度前缀和,2.激光炸弹

【算法通关指南:算法基础篇】 二维前缀和专题: 1. 【模板】二维度前缀和,2.激光炸弹

《算法通关指南:算法基础篇 ---- 二维前缀和 — 1. 【模板】二维度前缀和,2.激光炸弹》 🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南 》 ✨ 永远相信美好的事情即将发生 文章目录 * 《算法通关指南:算法基础篇 ---- 二维前缀和 — 1. 【模板】二维度前缀和,2.激光炸弹》 * 前言 * 一、二维前缀和 * 1.1 核心问题 * 1.1.1 创建前缀和矩阵 * 2.2.2 查询以(x1 , y1)为左上角,(x2 , y2)为右下角的子矩阵的和 * 二、

By Ne0inhk

傅里叶级数 傅里叶变换 离散时间傅里叶变换(DTFT) 离散傅里叶级数(DFS) 离散傅里叶变换(DFT)快速傅里叶变换(FFT)

傅里叶变换 傅里叶级数 FS 傅里叶变换 FT 时域采样 离散时间傅里叶变换 DTFT 时域采样 离散傅里叶级数 DFS 取有限长视为周期序列的主值周期 取其一个周期 离散傅里叶变换 DFT 频域采样 周期连续信号 离散非周期频谱 非周期连续信号 连续非周期频谱 非周期离散序列 连续周期频谱 周期离散序列 离散周期频谱

By Ne0inhk
初识数据结构——二叉树从基础概念到实践应用

初识数据结构——二叉树从基础概念到实践应用

数据结构专栏 ⬅(click) 初识二叉树:从基础概念到实践应用🌳 一、树型结构基础 1.1 树的基本概念 树是一种非线性的数据结构,由n(n>0)个有限节点组成一个具有层次关系的集合。它看起来像一棵倒挂的树,根朝上而叶朝下。 关键特点:有且仅有一个根节点,没有前驱节点除根节点外,其余节点被分成M(M>0)个互不相交的子树树是递归定义的 重要术语:结点的度:一个结点含有子树的个数树的度:树中所有结点度的最大值叶子结点:度为0的结点双亲结点/父结点:含有子结点的结点孩子结点/子结点:一个结点含有的子树的根结点根结点:没有双亲结点的结点结点的层次:从根开始定义,根为第1层树的高度/深度:树中结点的最大层次 1.2 树的表示方法 最常用的表示方法是孩子兄弟表示法: classNode{int value;// 树中存储的数据Node firstChild;// 第一个孩子引用Node nextBrother;

By Ne0inhk
【狂热算法篇】堆核驱动 TopK 分拣,快选奇招直击数据核心

【狂热算法篇】堆核驱动 TopK 分拣,快选奇招直击数据核心

在数据的浩瀚海洋里,我们常常会遇到这样一类需求:从大量数据中找出最大或最小的前 K 个元素,这就是 TopK 问题。比如在搜索引擎中,要从海量网页里筛选出与用户查询最相关的前 K 个结果;在电商平台,需统计出热销商品的前 K 名。解决 TopK 问题有多种方法,这里着重介绍快速选择法与堆法 。  欢迎拜访:羑悻的小杀马特.-ZEEKLOG博客 本篇主题:深度剖析TOP_K问题解答的快速选择法与堆法 制作日期:2025.05.21 隶属专栏:美妙的算法世界 目录 一.快速选择法: 1.1快选介绍: 1.2时间与空间复杂度分析: 1.2.1时间复杂度: 1.2.2空间复杂度:  1.3 代码实现快选法: top第k 大:

By Ne0inhk