跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
编程语言Node.js大前端

API 设计的 7 个致命错误与最佳实践指南

综述由AI生成总结了 API 设计中常见的 7 个致命错误,包括暴露数据库结构、HTTP 方法滥用、错误处理混乱、URL 设计不规范、缺乏版本控制、返回数据冗余以及分页排序不统一。针对每个问题提供了 RESTful 风格、标准化响应格式、字段筛选、通用分页实现等解决方案,并给出了 Python 和 Node.js 的代码示例,旨在帮助开发者构建更专业、易维护且高性能的接口。

云朵棉花糖发布于 2026/4/5更新于 2026/5/2226 浏览
API 设计的 7 个致命错误与最佳实践指南

API 设计的 7 个致命错误与最佳实践指南

后端服务常面临前端的质疑:

  • 为什么删除用户的接口是 POST?
  • 为什么获取用户列表要传 20 个参数?
  • 为什么同一个错误,有时返回 200,有时返回 500?
  • 能不能别再改接口了?这是这个月第三次了!

明明功能都实现了,为什么前端还是不满意?因为你的 API 设计,可能犯了这 7 个致命错误。

错误 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_hash、user_create_ts,这是给人看的还是给机器看的?
  2. 暴露实现细节:前端不需要知道你用的是时间戳还是日期字符串。
  3. 难以演进:数据库改个字段名,API 就得跟着改,前端也得改。
  4. 安全隐患:密码哈希值为什么要返回?

正确姿势

# ✅ 面向业务的 API 设计
GET /api/users/123
# 返回
{
  "id": 123,
  "username": "zhangsan",
  "displayName": "张三",
  "createdAt": "2024-01-01T00:00:00Z",
  "lastLoginAt": ,
  : ,
  : {
    : ,
    : 
  }
}
"2025-01-15T10:30:00Z"
"status"
"active"
"role"
"id"
5
"name"
"editor"

核心原则:API 是给开发者用的,不是给数据库用的。

# Python/Django 示例:用序列化器隐藏实现细节
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    # 重命名字段
    display_name = serializers.CharField(source='user_name')
    created_at = serializers.DateTimeField(source='user_create_ts')
    # 自定义字段
    status = serializers.SerializerMethodField()

    def get_status(self, obj):
        # 将数字状态码转换为语义化字符串
        return 'active' if obj.user_status_flag == 1 else 'inactive'

    class Meta:
        model = User
        fields = ['id', 'username', 'display_name', 'created_at', 'status']
        exclude = ['user_pwd_hash']
// Node.js/Express 示例
app.get("/api/users/:id", async (req, res) => {
  const user = await db.users.findById(req.params.id);
  // 转换数据格式
  res.json({
    id: user.user_id,
    username: user.user_name,
    displayName: user.user_name,
    createdAt: new Date(user.user_create_ts * 1000).toISOString(),
    status: user.user_status_flag === 1 ? "active" : "inactive",
  });
});

错误 2:HTTP 方法乱用,POST 包打天下

灾难现场

// ❌ 全部用 POST
POST /api/getUser      // 获取用户
POST /api/createUser   // 创建用户
POST /api/updateUser   // 更新用户
POST /api/deleteUser   // 删除用户
POST /api/searchUsers  // 搜索用户

为什么这样不好?

  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'])
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>...</html>

// 情况 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__)

# 自定义异常类
class APIError(Exception):
    def __init__(self, code, message, details=None, status_code=400):
        self.code = code
        self.message = message
        self.details = details
        self.status_code = status_code

# 全局错误处理器
@app.errorhandler(APIError)
def handle_api_error(error):
    response = {
        'error': {
            'code': error.code,
            'message': error.message,
            'details': error.details
        },
        'requestId': str(uuid.uuid4()),
        'timestamp': datetime.utcnow().isoformat() + 'Z'
    }
    return jsonify(response), error.status_code

@app.errorhandler(404)
def handle_not_found(error):
    return jsonify({'error': {'code': 'RESOURCE_NOT_FOUND', 'message': 'The requested resource was not found'}}), 404

@app.errorhandler(500)
def handle_internal_error(error):
    app.logger.error(f'Internal error: {error}')
    return jsonify({
        'error': {
            'code': 'INTERNAL_SERVER_ERROR',
            'message': 'An unexpected error occurred',
            'details': 'Please contact support if the problem persists'
        },
        'requestId': str(uuid.uuid4())
    }), 500

# 使用示例
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get(user_id)
    if not user:
        raise APIError(
            code='USER_NOT_FOUND',
            message='User not found',
            details=f'User with ID {user_id} does not exist',
            status_code=404
        )
    return jsonify(user.to_dict())
// Node.js/Express 示例
const express = require("express");
const { v4: uuidv4 } = require("uuid");
const app = express();

// 自定义错误类
class APIError extends Error {
  constructor(code, message, details, statusCode = 400) {
    super(message);
    this.code = code;
    this.details = details;
    this.statusCode = statusCode;
  }
}

// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error("Error:", err);
  if (err instanceof APIError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        details: err.details,
      },
      requestId: uuidv4(),
      timestamp: new Date().toISOString(),
    });
  }
  res.status(500).json({
    error: {
      code: "INTERNAL_SERVER_ERROR",
      message: "An unexpected error occurred",
      details: process.env.NODE_ENV === "development" ? err.message : undefined,
    },
    requestId: uuidv4(),
    timestamp: new Date().toISOString(),
  });
});

// 使用示例
app.get("/api/users/:id", async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      throw new APIError("USER_NOT_FOUND", "User not found", `User with ID ${req.params.id} does not exist`, 404);
    }
    res.json(user);
  } catch (error) {
    next(error);
  }
});

错误 4:URL 设计混乱不堪

灾难现场

// ❌ 各种混乱的 URL 设计
// 问题 1:动词 + 名词混用
GET /api/getUsers
GET /api/user/list
GET /api/fetchUserData

// 问题 2:过度嵌套
GET /api/v1/company/123/department/456/team/789/user/111/posts/222/comments/333

// 问题 3:命名不一致
GET /api/users     // 复数
GET /api/product   // 单数
GET /api/order-list // 短横线
GET /api/userProfile // 驼峰

// 问题 4:查询参数放在路径里
GET /api/users/search/name/zhangsan/age/25

正确姿势:清晰的 URL 设计

// ✅ 好的 URL 设计原则
// 1. 使用名词,不用动词
GET /api/users      // ✅
GET /api/getUsers   // ❌

// 2. 使用复数形式
GET /api/users      // ✅
GET /api/user       // ❌

// 3. 使用短横线分隔单词
GET /api/user-profiles // ✅
GET /api/userProfiles  // ❌
GET /api/user_profiles // ❌

// 4. 资源嵌套不超过 2 层
GET /api/users/123/posts      // ✅ 获取用户的文章
GET /api/posts?userId=123     // ✅ 也可以用查询参数
GET /api/users/123/posts/456/comments // ❌ 太深了

// 5. 查询、过滤、排序用查询参数
GET /api/users?name=zhangsan&age=25 // ✅
GET /api/users/search/name/zhangsan // ❌

// 6. 分页用查询参数
GET /api/users?page=1&limit=20 // ✅
GET /api/users/page/1/limit/20 // ❌
URL 设计速查表
场景推荐方式说明
获取列表GET /api/users使用复数名词
获取单个GET /api/users/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:没有版本控制,改接口全靠吼

灾难现场

// 第一版 API
GET /api/users/123
{
  "name": "张三",
  "age": 25
}

// 三个月后,需求变了,直接改接口
GET /api/users/123
{
  "firstName": "三",
  "lastName": "张",
  "birthDate": "1999-01-01"
}

// 结果:所有老版本的 App 全部崩溃

为什么需要版本控制?

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

正确姿势:API 版本控制

方案 1:URL 路径版本(推荐)

GET /api/v1/users/123
GET /api/v2/users/123

优点:

  • 清晰明了,一眼就能看出版本。
  • 容易在路由层面做版本隔离。
  • 方便缓存和 CDN。

缺点:

  • URL 会变化。

方案 2:请求头版本

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); // 默认使用最新版本
版本废弃策略
// 在响应头中标记废弃信息
X-API-Deprecated: true
X-API-Deprecation-Date: 2025-12-31
X-API-Sunset-Date: 2026-03-31
Link: </api/v2>; rel="successor-version"
// 在响应体中也可以包含废弃信息
{
  "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": { ... },
    "posts": [...],
    "followers": [...],
    "following": [...],
    "createdAt": "2024-01-01T00:00:00Z",
    "updatedAt": "2025-01-15T10:30:00Z",
    "lastLoginAt": "2025-01-17T08:00:00Z",
    "loginCount": 1234,
    "ipAddress": "192.168.1.1"
  }
]

问题:

  1. 响应体积巨大(可能几 MB)。
  2. 包含敏感信息。
  3. 前端只需要 id、username、email,其他都是浪费。
  4. 加载慢,用户体验差。

正确姿势:按需返回数据

方案 1:字段筛选(Sparse Fieldsets)

// 只返回需要的字段
GET /api/users?fields=id,username,email

// 支持嵌套字段
GET /api/users?fields=id,username,profile.avatar,profile.bio

方案 2:不同场景返回不同数据

// 列表视图:只返回摘要信息
GET /api/users

// 详情视图:返回完整信息
GET /api/users/1

方案 3:使用 GraphQL(终极方案)

# 前端精确指定需要的字段
query { users { id username email } }

# 需要更多信息时
query { users { id username email profile { avatar bio } posts(limit: 5) { id title } } }

实战代码:字段筛选实现

# Python/Flask 示例
from flask import Flask, request, jsonify

app = Flask(__name__)

def filter_fields(data, fields):
    if not fields:
        return data
    field_list = fields.split(',')
    if isinstance(data, list):
        return [filter_fields(item, fields) for item in data]
    if isinstance(data, dict):
        filtered = {}
        for field in field_list:
            if '.' in field:
                parts = field.split('.', 1)
                parent, child = parts[0], parts[1]
                if parent in data:
                    if parent not in filtered:
                        filtered[parent] = {}
                    filtered[parent].update(filter_fields(data[parent], child))
            elif field in data:
                filtered[field] = data[field]
        return filtered
    return data

@app.route('/api/users')
def get_users():
    users = User.query.all()
    data = [user.to_dict() for user in users]
    fields = request.args.get('fields')
    if fields:
        data = filter_fields(data, fields)
    return jsonify(data)
// Node.js/Express 示例
const express = require("express");
const app = express();

function 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;
}

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();
}

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

为什么需要统一?

  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)

def paginate(query, page=1, limit=20):
    total = query.count()
    total_pages = (total + limit - 1) // limit
    items = query.offset((page - 1) * limit).limit(limit).all()
    return {
        'data': [item.to_dict() for item in items],
        'pagination': {
            'page': page,
            'limit': limit,
            'total': total,
            'totalPages': total_pages,
            'hasNext': page < total_pages,
            'hasPrev': page > 1
        }
    }

@app.route('/api/users')
def get_users():
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 20, type=int)
    sort = request.args.get('sort', 'id')
    order = request.args.get('order', 'asc')
    search = request.args.get('search', '')
    status = request.args.get('status', '')

    query = User.query
    if search:
        query = query.filter(User.username.like(f'%{search}%'))
    if status:
        query = query.filter(User.status == status)
    if order == 'desc':
        query = query.order_by(getattr(User, sort).desc())
    else:
        query = query.order_by(getattr(User, sort).asc())

    result = paginate(query, page, limit)
    return jsonify(result)
// Node.js/Express + Sequelize 示例
const express = require("express");
const { Op } = require("sequelize");
const app = express();

async function paginate(model, options = {}) {
  const { page = 1, limit = 20, where = {}, order = [["id", "ASC"]], attributes, } = options;
  const offset = (page - 1) * limit;
  const { count, rows } = await model.findAndCountAll({ where, order, limit, offset, attributes });
  const totalPages = Math.ceil(count / limit);
  return {
    data: rows,
    pagination: {
      page,
      limit,
      total: count,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1,
    },
  };
}

app.get("/api/users", async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const search = req.query.search || "";
    const status = req.query.status || "";
    const sort = req.query.sort || "id";
    const order = req.query.order || "asc";

    const where = {};
    if (search) {
      where.username = { [Op.like]: `%${search}%` };
    }
    if (status) {
      where.status = status;
    }

    const orderBy = [[sort, order.toUpperCase()]];
    const result = await paginate(User, { page, limit, where, order: orderBy });
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
高级过滤语法
// 等于
GET /api/users?status=active
// 不等于
GET /api/users?status[ne]=inactive
// 大于/小于
GET /api/users?age[gt]=18&age[lt]=60
// 大于等于/小于等于
GET /api/users?age[gte]=18&age[lte]=60
// 包含(数组)
GET /api/users?role[in]=admin,editor
// 不包含
GET /api/users?role[nin]=guest
// 模糊搜索
GET /api/users?username[like]=zhang
// 日期范围
GET /api/users?createdAt[gte]=2024-01-01&createdAt[lte]=2024-12-31
// 多个条件组合
GET /api/users?status=active&age[gte]=18&role[in]=admin,editor&sort=-createdAt

总结:API 设计的黄金法则

  1. 以开发者为中心:API 是给开发者用的,不是给数据库用的。设计时要站在使用者的角度思考。
  2. 保持一致性:命名规范、错误格式、分页参数…所有接口都应该遵循相同的规范。
  3. 使用标准:HTTP 方法、状态码、日期格式…尽量使用业界标准,不要自己发明。
  4. 文档先行:好的文档胜过千言万语。使用 OpenAPI/Swagger 自动生成文档。
  5. 版本控制:从第一天就考虑版本控制,不要等到需要改接口时才想起来。
  6. 性能优化:分页、字段筛选、缓存…从设计阶段就考虑性能问题。
  7. 安全第一:认证、授权、限流、数据脱敏…安全永远是第一位的。

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: 20
      responses:
        "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 层出不穷
  • 让新人一脸懵逼
  • 让产品迭代举步维艰

当前环境下,应致力于编写高质量的 API。

记住这 7 个致命错误:

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

避开这些坑,你的 API 就能让前端竖起大拇指。

完整示例

// 用户管理 API - 完整示例
// ============ 基础 CRUD ============
// 获取用户列表(支持分页、搜索、过滤、排序)
GET /api/v1/users?page=1&limit=20&search=zhang&status=active&sort=-createdAt
Response: 200 OK
{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}

// 获取单个用户
GET /api/v1/users/123
Response: 200 OK
{
  "id": 123,
  "username": "zhangsan",
  "email": "[email protected]",
  "profile": {...},
  "createdAt": "2024-01-01T00:00:00Z"
}

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

// 更新用户
PATCH /api/v1/users/123
Request Body: {"email": "[email protected]"}
Response: 200 OK
{
  "id": 123,
  "username": "zhangsan",
  "email": "[email protected]",
  "updatedAt": "2025-01-17T10:35:00Z"
}

// 删除用户
DELETE /api/v1/users/123
Response: 204 No Content

// ============ 关联资源 ============
// 获取用户的文章
GET /api/v1/users/123/posts?page=1&limit=10

// 获取用户的关注者
GET /api/v1/users/123/followers

// 关注用户
POST /api/v1/users/123/followers
Request Body: {"userId": 456}

// 取消关注
DELETE /api/v1/users/123/followers/456

// ============ 错误响应 ============
// 400 Bad Request
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [{"field": "email", "message": "Email format is invalid"}]
  },
  "requestId": "req_abc123",
  "timestamp": "2025-01-17T10:30:00Z"
}

// 404 Not Found
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found",
    "details": "User with ID 123 does not exist"
  },
  "requestId": "req_abc123",
  "timestamp": "2025-01-17T10:30:00Z"
}

现在,去设计一个让人赞不绝口的 API 吧!

目录

  1. API 设计的 7 个致命错误与最佳实践指南
  2. 错误 1:把数据库表结构直接暴露成 API
  3. 灾难现场
  4. ❌ 直接暴露数据库结构
  5. 返回
  6. 正确姿势
  7. ✅ 面向业务的 API 设计
  8. 返回
  9. Python/Django 示例:用序列化器隐藏实现细节
  10. 错误 2:HTTP 方法乱用,POST 包打天下
  11. 灾难现场
  12. 正确姿势:RESTful 风格
  13. 实战代码
  14. Python/Flask 示例
  15. 获取用户列表
  16. 获取单个用户
  17. 创建用户
  18. 完整更新用户
  19. 部分更新用户
  20. 删除用户
  21. 特殊场景:非 CRUD 操作怎么办?
  22. 错误 3:错误处理一团糟
  23. 灾难现场
  24. 正确姿势:标准化的错误响应
  25. HTTP 状态码速查表
  26. 实战代码:统一错误处理
  27. Python/Flask 示例
  28. 自定义异常类
  29. 全局错误处理器
  30. 使用示例
  31. 错误 4:URL 设计混乱不堪
  32. 灾难现场
  33. 正确姿势:清晰的 URL 设计
  34. URL 设计速查表
  35. 实战示例:完整的用户 API
  36. 错误 5:没有版本控制,改接口全靠吼
  37. 灾难现场
  38. 正确姿势:API 版本控制
  39. 实战代码:版本控制实现
  40. Python/Flask 示例
  41. 方案 1:URL 路径版本
  42. 方案 2:请求头版本
  43. 版本废弃策略
  44. 错误 6:返回数据一股脑全给,前端要啥给啥
  45. 灾难现场
  46. 正确姿势:按需返回数据
  47. 前端精确指定需要的字段
  48. 需要更多信息时
  49. 实战代码:字段筛选实现
  50. Python/Flask 示例
  51. 错误 7:分页、排序、过滤各自为政
  52. 灾难现场
  53. 正确姿势:统一的查询规范
  54. 标准响应格式
  55. 实战代码:通用分页实现
  56. Python/Flask 示例
  57. 高级过滤语法
  58. 总结:API 设计的黄金法则
  59. API 设计检查清单
  60. 推荐工具
  61. API 设计工具
  62. openapi.yaml 示例
  63. API 网关
  64. 监控工具
  65. 结语:好的 API 是产品的一部分
  66. 完整示例
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 31 岁转行软件测试:一位 34 岁从业者的经历与感悟
  • Qwen3-TTS VoiceDesign 在虚拟现实中的沉浸式语音应用
  • 一人一周重构开源官网:AI 驱动的技术与效率革命
  • 大模型应用架构:从零解读检索增强生成 RAG
  • 知网 AIGC 检测算法 2026 升级:新规则解读与应对策略
  • 基于 Spring Boot 与 Vue 的无人机共享管理系统设计与实现
  • 论文降重与 AIGC 检测双重达标的技术方案
  • C++ 类和对象:默认成员函数详解
  • 科学机器学习中的物理信息神经网络:现状与展望
  • Python 数据分析入门:集中趋势与离散程度详解
  • 信创国产化开发为何推荐使用 Java
  • 程序员如何规避 35 岁职业危机
  • AIGC 自动化编程实践:基于 ChatGPT 与 GitHub Copilot 阅读笔记
  • go-zero 微服务架构入门与实践
  • JavaScript 设计模式:策略模式实战案例解析
  • Spring AI MCP Server 集成与示例
  • 基于 DeepFace 和 OpenCV 的情绪分析器
  • 前端开发必备技能:AI 设计优化、工程实践与硬件效率提升
  • Ubuntu 下 llama.cpp 编译构建与性能调优实战
  • GitHub 全界面中文化插件安装与配置指南

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online