欢迎光临
我们一直在努力

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

    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 # 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__)

    # 自定义异常类
    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/userprofiles // ✅
    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" // age 改成 birthDate!
    }

    // 结果:所有老版本的 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, # v1 返回完整姓名
    '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, # v2 拆分姓名
    'lastName': user.last_name,
    'birthDate': user.birth_date.isoformat() # v2 返回生日而不是年龄
    })

    # 方案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": "zhangsan@example.com",
    "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": "zhangsan@example.com"
    },
    {
    "id": 2,
    "username": "lisi",
    "email": "lisi@example.com"
    }
    ]

    // 支持嵌套字段
    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": "zhangsan@example.com",
    "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):
    """根据 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:
    # 处理嵌套字段,如 profile.avatar
    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

    // 前端开发者:我 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)

    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"

    // 构建 where 条件
    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]=20240101&createdAt[lte]=20241231

    // 多个条件组合
    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: 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": "zhangsan@example.com",
    "profile": {},
    "createdAt": "2024-01-01T00:00:00Z"
    }

    // 创建用户
    POST /api/v1/users
    Request Body:
    {
    "username": "zhangsan",
    "email": "zhangsan@example.com",
    "password": "********"
    }
    Response: 201 Created
    {
    "id": 123,
    "username": "zhangsan",
    "email": "zhangsan@example.com",
    "createdAt": "2025-01-17T10:30:00Z"
    }

    // 更新用户
    PATCH /api/v1/users/123
    Request Body:
    {
    "email": "newemail@example.com"
    }
    Response: 200 OK
    {
    "id": 123,
    "username": "zhangsan",
    "email": "newemail@example.com",
    "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 吧!🚀

    赞(0)
    未经允许不得转载:171主机测评 » API 设计的 7 个致命错误:为什么你的接口让前端想打人
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址