普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月19日首页

API 设计最佳实践 Javascript 篇

作者 召摇
2025年9月18日 19:16

API 设计最佳实践 Javascript 篇

在当今的数字化时代,API(应用程序编程接口)已成为软件系统之间通信的基石。一个良好设计的 API 不仅能提高开发效率,还能增强系统的可扩展性和可维护性。本文将深入探讨 API 设计的七大最佳实践,通过理论解析、代码实现和实际案例,帮助您构建健壮、高效且易于使用的 API。

1. REST 基础原理

1.1 什么是 REST?

REST(Representational State Transfer,表述性状态转移)是一种基于 HTTP 协议的架构风格,由 Roy Fielding 在 2000 年提出。它通过统一的接口和标准化的操作,使系统之间的通信更加简单和可预测。

核心特征

  • 无状态性:每个请求包含所有必要信息,服务器不存储客户端状态
  • 可缓存性:响应必须明确表明是否可缓存
  • 分层系统:客户端无需了解是否直接连接最终服务器
  • 统一接口:简化系统架构,改善组件间交互可见性

1.2 REST 资源设计

在 REST 架构中,一切都被视为资源,每个资源都有唯一的标识符(URI)。正确的资源设计是 RESTful API 成功的关键。

资源命名最佳实践

  • 使用名词而非动词(/users 而不是 /getUsers
  • 使用复数形式表示资源集合
  • 保持一致性的大小写(推荐小写和连字符)
  • 避免文件扩展名(使用 Accept header 指定格式)
// 良好的资源设计示例
const express = require('express');
const app = express();

// 用户资源集合
app.get('/users', (req, res) => {
  // 获取用户列表
  const users = UserModel.findAll();
  res.json(users);
});

// 特定用户资源
app.get('/users/:userId', (req, res) => {
  // 根据ID获取特定用户
  const user = UserModel.findById(req.params.userId);
  res.json(user);
});

// 用户下的订单子资源
app.get('/users/:userId/orders', (req, res) => {
  // 获取用户的订单列表
  const orders = OrderModel.findByUser(req.params.userId);
  res.json(orders);
});

1.3 HTTP 方法详解

RESTful API 充分利用 HTTP 方法的语义,使 API 更加直观和自描述。

HTTP 方法 语义 幂等性 安全性
GET 检索资源
POST 创建新资源
PUT 更新或替换资源
PATCH 部分更新资源
DELETE 删除资源

实战示例:完整的 CRUD 操作

// 用户资源完整的 CRUD 实现
app.post('/users', (req, res) => {
  // 创建新用户
  const newUser = UserModel.create(req.body);
  res.status(201).json(newUser);
});

app.get('/users/:id', (req, res) => {
  // 获取用户信息
  const user = UserModel.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: '用户不存在' });
  }
  res.json(user);
});

app.put('/users/:id', (req, res) => {
  // 完全更新用户信息
  const updatedUser = UserModel.update(req.params.id, req.body);
  res.json(updatedUser);
});

app.patch('/users/:id', (req, res) => {
  // 部分更新用户信息
  const updatedUser = UserModel.partialUpdate(req.params.id, req.body);
  res.json(updatedUser);
});

app.delete('/users/:id', (req, res) => {
  // 删除用户
  UserModel.delete(req.params.id);
  res.status(204).send();
});

1.4 超媒体作为应用状态引擎(HATEOAS)

HATEOAS 是 REST 架构的一个关键约束,它使客户端能够通过服务器提供的超媒体动态发现可用的操作。

// HATEOAS 示例实现
app.get('/users/:id', (req, res) => {
  const user = UserModel.findById(req.params.id);
  
  const response = {
    id: user.id,
    name: user.name,
    email: user.email,
    links: [
      { rel: 'self', href: `/users/${user.id}`, method: 'GET' },
      { rel: 'update', href: `/users/${user.id}`, method: 'PUT' },
      { rel: 'delete', href: `/users/${user.id}`, method: 'DELETE' },
      { rel: 'orders', href: `/users/${user.id}/orders`, method: 'GET' }
    ]
  };
  
  res.json(response);
});

2. 错误处理机制

2.1 HTTP 状态码的正确使用

HTTP 状态码是客户端了解请求结果的首要方式。正确使用状态码对于 API 的可用性至关重要。

主要状态码类别

  • 1xx:信息性响应
  • 2xx:成功响应
  • 3xx:重定向
  • 4xx:客户端错误
  • 5xx:服务器错误

常用状态码详解

状态码 含义 使用场景
200 OK 请求成功 成功的 GET、PUT、PATCH 请求
201 Created 资源创建成功 成功的 POST 请求
204 No Content 成功但无内容返回 成功的 DELETE 请求
400 Bad Request 错误请求 请求参数错误或格式不正确
401 Unauthorized 未认证 需要身份验证但未提供
403 Forbidden 禁止访问 身份验证成功但无权限
404 Not Found 资源不存在 请求的资源不存在
429 Too Many Requests 请求过多 超出速率限制
500 Internal Server Error 服务器内部错误 服务器端未处理的异常

2.2 错误响应格式标准化

一致的错误响应格式帮助客户端统一处理各种错误情况。

// 统一的错误处理中间件
app.use((err, req, res, next) => {
  console.error('错误详情:', err);
  
  // 根据不同错误类型返回相应的状态码和消息
  let statusCode = 500;
  let errorCode = 'INTERNAL_ERROR';
  let message = '服务器内部错误';
  
  if (err.name === 'ValidationError') {
    statusCode = 400;
    errorCode = 'VALIDATION_ERROR';
    message = '输入数据验证失败';
  } else if (err.name === 'NotFoundError') {
    statusCode = 404;
    errorCode = 'NOT_FOUND';
    message = '请求的资源不存在';
  } else if (err.name === 'AuthenticationError') {
    statusCode = 401;
    errorCode = 'UNAUTHORIZED';
    message = '身份验证失败';
  } else if (err.name === 'AuthorizationError') {
    statusCode = 403;
    errorCode = 'FORBIDDEN';
    message = '没有访问权限';
  }
  
  // 结构化错误响应
  res.status(statusCode).json({
    error: {
      code: errorCode,
      message: message,
      details: process.env.NODE_ENV === 'development' ? err.message : undefined,
      timestamp: new Date().toISOString(),
      traceId: req.id // 请求追踪ID
    }
  });
});

// 自定义错误类
class AppError extends Error {
  constructor(name, message, statusCode) {
    super(message);
    this.name = name;
    this.statusCode = statusCode;
  }
}

class ValidationError extends AppError {
  constructor(message = '验证失败') {
    super('ValidationError', message, 400);
  }
}

2.3 验证和业务错误处理

输入验证是 API 安全性和稳定性的第一道防线。

// 使用 Joi 进行请求验证
const Joi = require('joi');

// 用户创建验证规则
const userCreateSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])')).required(),
  age: Joi.number().integer().min(18).max(120).optional()
});

// 验证中间件
const validate = (schema) => (req, res, next) => {
  const { error, value } = schema.validate(req.body, {
    abortEarly: false, // 返回所有验证错误
    allowUnknown: true, // 允许未知字段(将被忽略)
    stripUnknown: true // 移除未知字段
  });
  
  if (error) {
    const errorDetails = error.details.map(detail => ({
      field: detail.path.join('.'),
      message: detail.message,
      type: detail.type
    }));
    
    throw new ValidationError('输入数据验证失败', errorDetails);
  }
  
  req.body = value;
  next();
};

// 在路由中使用验证
app.post('/users', validate(userCreateSchema), (req, res) => {
  // 处理已验证的请求数据
  const user = UserModel.create(req.body);
  res.status(201).json(user);
});

3. API 版本控制

3.1 版本控制策略

API 版本控制是维护向后兼容性的关键策略,确保现有客户端在 API 演进过程中不受影响。

三种主要版本控制方法

::: tabs

@tab URL 路径版本控制

// 版本控制中间件
app.use('/api/v1', require('./routes/v1/users'));
app.use('/api/v2', require('./routes/v2/users'));

// v1 用户路由
// 路径: /api/v1/users
router.get('/', (req, res) => {
  // v1 实现
});

// v2 用户路由  
// 路径: /api/v2/users
router.get('/', (req, res) => {
  // v2 实现 - 可能包含破坏性变更
});

@tab HTTP 头版本控制

// 基于 Accept 头的版本控制中间件
const versionMiddleware = (req, res, next) => {
  const acceptHeader = req.get('Accept') || '';
  const versionMatch = acceptHeader.match(/application\/vnd\.api\.v(\d+)\+json/);
  
  if (versionMatch) {
    req.apiVersion = parseInt(versionMatch[1], 10);
  } else {
    // 默认版本
    req.apiVersion = 1;
  }
  
  next();
};

// 使用版本控制
app.use(versionMiddleware);
app.use('/api/users', (req, res) => {
  if (req.apiVersion === 1) {
    // v1 逻辑
  } else if (req.apiVersion === 2) {
    // v2 逻辑
  }
});

@tab 查询参数版本控制

// 不推荐的查询参数版本控制
app.get('/api/users', (req, res) => {
  const version = req.query.version || '1';
  
  if (version === '1') {
    // v1 实现
  } else if (version === '2') {
    // v2 实现
  }
});

:::

3.2 版本迁移策略

当引入破坏性变更时,需要制定周密的迁移计划。

graph TD
    A[现有 v1 API] --> B{评估变更影响}
    B --> C[非破坏性变更]
    B --> D[破坏性变更]
    
    C --> E[直接部署到 v1]
    D --> F[创建 v2 分支]
    
    F --> G[实现新功能]
    G --> H[并行运行 v1 和 v2]
    
    H --> I[通知客户端迁移]
    I --> J[设置弃用时间表]
    
    J --> K{所有客户端迁移完成?}
    K -->|否| J
    K -->|是| L[停用 v1 API]
    
    style E fill:#90EE90
    style L fill:#FFB6C1

3.3 版本管理最佳实践

// API 版本管理配置
const apiVersions = {
  current: 2,
  supported: [1, 2],
  deprecated: [1], // 已弃用但仍支持的版本
  sunset: {
    1: new Date('2024-12-31') // v1 的 sunset 日期
  }
};

// 版本信息中间件
app.use((req, res, next) => {
  res.set({
    'API-Version': apiVersions.current,
    'Supported-Versions': apiVersions.supported.join(', '),
    'Deprecated-Versions': apiVersions.deprecated.join(', '),
    'Sunset': apiVersions.deprecated.length > 0 ? 
      `Version 1 will be sunset on ${apiVersions.sunset[1].toISOString()}` : ''
  });
  next();
});

// 弃用警告中间件
app.use('/api/v1/*', (req, res, next) => {
  res.set({
    'Warning': `299 - "Version 1 is deprecated. Please migrate to version ${apiVersions.current} by ${apiVersions.sunset[1].toISOString()}"`
  });
  next();
});

4. 速率限制

4.1 速率限制算法

速率限制保护 API 免受滥用和过载,确保服务的可用性。

常见算法比较

::: tabs

@tab 令牌桶算法

class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;        // 桶容量
    this.tokens = capacity;           // 当前令牌数
    this.refillRate = refillRate;     // 每秒补充速率
    this.lastRefill = Date.now();    // 上次补充时间
  }
  
  refill() {
    const now = Date.now();
    const timePassed = (now - this.lastRefill) / 1000;
    const tokensToAdd = timePassed * this.refillRate;
    
    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
  
  consume(tokens = 1) {
    this.refill();
    
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true; // 请求允许
    }
    
    return false; // 请求拒绝
  }
}

// 使用令牌桶的中间件
const rateLimitMiddleware = (req, res, next) => {
  const clientId = req.get('API-Key') || req.ip;
  
  if (!rateLimiters[clientId]) {
    // 每个客户端独立的桶:10个令牌,每秒补充2个
    rateLimiters[clientId] = new TokenBucket(10, 2);
  }
  
  if (rateLimiters[clientId].consume()) {
    next();
  } else {
    res.status(429).json({
      error: '请求过于频繁',
      retryAfter: calculateRetryAfter(rateLimiters[clientId])
    });
  }
};

@tab 滑动窗口算法

class SlidingWindow {
  constructor(windowSize, maxRequests) {
    this.windowSize = windowSize;    // 窗口大小(毫秒)
    this.maxRequests = maxRequests;  // 窗口内最大请求数
    this.requests = [];             // 请求时间戳数组
  }
  
  addRequest() {
    const now = Date.now();
    this.requests.push(now);
    
    // 移除超出窗口的旧请求
    const windowStart = now - this.windowSize;
    this.requests = this.requests.filter(time => time > windowStart);
    
    return this.requests.length <= this.maxRequests;
  }
}

// Redis 实现的分布式滑动窗口
const redis = require('redis');
const client = redis.createClient();

const slidingWindowRedis = async (key, windowSize, maxRequests) => {
  const now = Date.now();
  const windowStart = now - windowSize;
  
  // 使用 Redis 有序集合存储请求时间戳
  await client.zadd(key, now, now.toString());
  
  // 移除旧请求
  await client.zremrangebyscore(key, 0, windowStart);
  
  // 获取当前窗口内的请求数量
  const requestCount = await client.zcard(key);
  
  // 设置键的过期时间
  await client.expire(key, windowSize / 1000);
  
  return requestCount <= maxRequests;
};

@tab 漏桶算法

class LeakyBucket {
  constructor(capacity, leakRate) {
    this.capacity = capacity;        // 桶容量
    this.water = 0;                  // 当前水量
    this.leakRate = leakRate;        // 漏水速率(请求/秒)
    this.lastLeak = Date.now();     // 上次漏水时间
  }
  
  leak() {
    const now = Date.now();
    const timePassed = (now - this.lastLeak) / 1000;
    const waterToRemove = timePassed * this.leakRate;
    
    this.water = Math.max(0, this.water - waterToRemove);
    this.lastLeak = now;
  }
  
  addWater(amount = 1) {
    this.leak();
    
    if (this.water + amount <= this.capacity) {
      this.water += amount;
      return true; // 请求进入队列
    }
    
    return false; // 请求被拒绝
  }
}

:::

4.2 多维度速率限制

在实际应用中,通常需要基于多个维度进行速率限制。

// 多维度速率限制配置
const rateLimitConfig = {
  // IP 基础限制
  ip: {
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100 // 最大请求数
  },
  
  // API 密钥限制(更宽松)
  apiKey: {
    windowMs: 15 * 60 * 1000,
    max: 1000
  },
  
  // 特定端点限制
  endpoints: {
    '/auth/login': {
      windowMs: 60 * 60 * 1000, // 1小时
      max: 5 // 登录尝试限制
    },
    '/api/payments': {
      windowMs: 60 * 1000, // 1分钟
      max: 10 // 支付请求限制
    }
  },
  
  // 全局限制(防止DDoS)
  global: {
    windowMs: 1000, // 1秒
    max: 500 // 全局最大请求数
  }
};

// 分层速率限制中间件
const layeredRateLimit = async (req, res, next) => {
  const clientIp = req.ip;
  const apiKey = req.get('API-Key');
  const endpoint = req.path;
  
  try {
    // 检查全局限制
    const globalAllowed = await checkRateLimit('global:', rateLimitConfig.global);
    if (!globalAllowed) {
      return res.status(429).json({ error: '系统繁忙,请稍后再试' });
    }
    
    // 检查IP限制
    const ipAllowed = await checkRateLimit(`ip:${clientIp}`, rateLimitConfig.ip);
    if (!ipAllowed) {
      return res.status(429).json({ error: 'IP请求过于频繁' });
    }
    
    // 检查API密钥限制(如果存在)
    if (apiKey) {
      const keyAllowed = await checkRateLimit(`key:${apiKey}`, rateLimitConfig.apiKey);
      if (!keyAllowed) {
        return res.status(429).json({ error: 'API密钥限额已用尽' });
      }
    }
    
    // 检查特定端点限制
    if (rateLimitConfig.endpoints[endpoint]) {
      const endpointKey = apiKey ? `endpoint:${apiKey}:${endpoint}` : `endpoint:${clientIp}:${endpoint}`;
      const endpointAllowed = await checkRateLimit(endpointKey, rateLimitConfig.endpoints[endpoint]);
      
      if (!endpointAllowed) {
        return res.status(429).json({ 
          error: `端点 ${endpoint} 请求过于频繁`,
          retryAfter: rateLimitConfig.endpoints[endpoint].windowMs / 1000
        });
      }
    }
    
    next();
  } catch (error) {
    console.error('速率限制错误:', error);
    next(); // 出错时放行请求,避免因限制系统故障影响正常服务
  }
};

4.3 速率限制响应头

提供详细的速率限制信息帮助客户端合理调整请求频率。

// 速率限制响应头中间件
const rateLimitHeaders = (req, res, next) => {
  const oldSend = res.send;
  
  res.send = function(data) {
    const clientId = req.get('API-Key') || req.ip;
    const rateLimiter = rateLimiters[clientId];
    
    if (rateLimiter) {
      res.set({
        'X-RateLimit-Limit': rateLimiter.capacity,
        'X-RateLimit-Remaining': Math.floor(rateLimiter.tokens),
        'X-RateLimit-Reset': Math.ceil((rateLimiter.capacity - rateLimiter.tokens) / rateLimiter.refillRate),
        'Retry-After': calculateRetryAfter(rateLimiter)
      });
    }
    
    return oldSend.call(this, data);
  };
  
  next();
};

// 计算重试时间
const calculateRetryAfter = (rateLimiter) => {
  const tokensNeeded = 1; // 需要1个令牌才能继续
  const deficit = tokensNeeded - rateLimiter.tokens;
  
  if (deficit <= 0) return 0;
  
  return Math.ceil(deficit / rateLimiter.refillRate);
};

5. 分页技术

5.1 偏移分页 vs 游标分页

分页是处理大型数据集的关键技术,不同的分页策略适用于不同的场景。

对比分析

graph LR
    A[分页技术选择] --> B{数据集特征}
    
    B --> C[小型数据集<br/>随机访问需求<br/>简单实现]
    B --> D[大型数据集<br/>顺序访问<br/>性能要求高]
    
    C --> E[偏移分页 Offset-based]
    D --> F[游标分页 Cursor-based]
    
    E --> G[优点: 实现简单, 支持随机跳页]
    E --> H[缺点: 性能问题, 数据一致性风险]
    
    F --> I[优点: 高性能, 数据一致性]
    F --> J[缺点: 实现复杂, 不支持随机访问]
    
    style E fill:#90EE90
    style F fill:#90EE90
    style G fill:#ADD8E6
    style H fill:#FFB6C1
    style I fill:#ADD8E6  
    style J fill:#FFB6C1

5.2 偏移分页实现

偏移分页是最传统和广泛使用的分页方法,适用于大多数中小型数据集。

// 偏移分页实现
const getUsersWithOffset = async (page = 1, limit = 10, filters = {}) => {
  const offset = (page - 1) * limit;
  
  // 获取总数
  const countQuery = UserModel.query()
    .where(filters)
    .count('* as total');
  
  // 获取分页数据
  const dataQuery = UserModel.query()
    .where(filters)
    .orderBy('created_at', 'desc')
    .offset(offset)
    .limit(limit);
  
  const [totalResult, users] = await Promise.all([countQuery, dataQuery]);
  const total = parseInt(totalResult[0].total, 10);
  const totalPages = Math.ceil(total / limit);
  
  return {
    data: users,
    pagination: {
      current_page: page,
      per_page: limit,
      total: total,
      total_pages: totalPages,
      has_prev: page > 1,
      has_next: page < totalPages,
      prev_page: page > 1 ? page - 1 : null,
      next_page: page < totalPages ? page + 1 : null
    }
  };
};

// 使用示例
app.get('/users', async (req, res) => {
  try {
    const page = parseInt(req.query.page, 10) || 1;
    const limit = parseInt(req.query.limit, 10) || 10;
    const filters = parseFilters(req.query);
    
    const result = await getUsersWithOffset(page, limit, filters);
    
    res.json({
      data: result.data,
      meta: {
        pagination: result.pagination,
        filters: filters
      },
      links: generatePaginationLinks(req, result.pagination)
    });
  } catch (error) {
    res.status(500).json({ error: '获取用户列表失败' });
  }
});

// 生成分页链接
const generatePaginationLinks = (req, pagination) => {
  const baseUrl = `${req.protocol}://${req.get('host')}${req.path}`;
  const queryParams = new URLSearchParams(req.query);
  
  const links = {
    first: `${baseUrl}?${queryParams.toString()}`,
    last: null,
    prev: null,
    next: null
  };
  
  if (pagination.has_prev) {
    queryParams.set('page', pagination.prev_page);
    links.prev = `${baseUrl}?${queryParams.toString()}`;
  }
  
  if (pagination.has_next) {
    queryParams.set('page', pagination.next_page);
    links.next = `${baseUrl}?${queryParams.toString()}`;
  }
  
  queryParams.set('page', pagination.total_pages);
  links.last = `${baseUrl}?${queryParams.toString()}`;
  
  return links;
};

5.3 游标分页实现

游标分页解决了偏移分页的性能和数据一致性问题,特别适合大型数据集和无限滚动场景。

// 游标分页实现
const getUsersWithCursor = async (cursor = null, limit = 10, direction = 'after', filters = {}) => {
  let query = UserModel.query()
    .where(filters)
    .orderBy('created_at', 'desc')
    .orderBy('id', 'desc'); // 二级排序确保唯一性
  
  // 应用游标条件
  if (cursor) {
    const cursorValue = await decodeCursor(cursor);
    
    if (direction === 'after') {
      query = query.where('created_at', '<', cursorValue.created_at)
                   .orWhere(function() {
                     this.where('created_at', '=', cursorValue.created_at)
                         .where('id', '<', cursorValue.id);
                   });
    } else if (direction === 'before') {
      query = query.where('created_at', '>', cursorValue.created_at)
                   .orWhere(function() {
                     this.where('created_at', '=', cursorValue.created_at)
                         .where('id', '>', cursorValue.id);
                   });
    }
  }
  
  // 获取limit+1条记录来判断是否有更多数据
  const users = await query.limit(limit + 1);
  
  const hasMore = users.length > limit;
  const items = hasMore ? users.slice(0, limit) : users;
  
  // 生成游标
  let startCursor = null;
  let endCursor = null;
  
  if (items.length > 0) {
    const firstItem = items[0];
    const lastItem = items[items.length - 1];
    
    startCursor = await encodeCursor({
      created_at: firstItem.created_at,
      id: firstItem.id
    });
    
    endCursor = await encodeCursor({
      created_at: lastItem.created_at,
      id: lastItem.id
    });
  }
  
  return {
    items,
    pageInfo: {
      hasNextPage: hasMore,
      hasPreviousPage: cursor !== null,
      startCursor,
      endCursor
    }
  };
};

// 游标编码解码(使用Base64)
const encodeCursor = async (cursorObject) => {
  return Buffer.from(JSON.stringify(cursorObject)).toString('base64');
};

const decodeCursor = async (cursorString) => {
  return JSON.parse(Buffer.from(cursorString, 'base64').toString());
};

// 使用示例
app.get('/users/cursor', async (req, res) => {
  try {
    const { after, before, first = 10, last } = req.query;
    const filters = parseFilters(req.query);
    
    let result;
    if (after) {
      result = await getUsersWithCursor(after, parseInt(first), 'after', filters);
    } else if (before) {
      result = await getUsersWithCursor(before, parseInt(last), 'before', filters);
    } else {
      result = await getUsersWithCursor(null, parseInt(first), 'after', filters);
    }
    
    res.json({
      data: result.items,
      pageInfo: result.pageInfo,
      filters
    });
  } catch (error) {
    res.status(400).json({ error: '无效的游标参数' });
  }
});

5.4 分页策略选择指南

选择合适的分页策略需要考虑多个因素:

// 分页策略选择函数
const choosePaginationStrategy = (datasetCharacteristics) => {
  const {
    totalSize,
    updateFrequency,
    accessPattern,
    consistencyRequirements,
    performanceNeeds
  } = datasetCharacteristics;
  
  if (totalSize < 1000 && updateFrequency === 'low') {
    return {
      strategy: 'offset',
      reason: '小型数据集,偏移分页简单有效',
      recommendations: [
        '使用标准的page/limit参数',
        '提供总数和总页数信息',
        '包含导航链接'
      ]
    };
  }
  
  if (totalSize > 10000 || updateFrequency === 'high') {
    return {
      strategy: 'cursor',
      reason: '大型或频繁更新的数据集,游标分页性能更佳',
      recommendations: [
        '使用after/before游标参数',
        '基于时间戳或序列ID创建游标',
        '提供hasNextPage/hasPreviousPage信息'
      ]
    };
  }
  
  if (accessPattern === 'random') {
    return {
      strategy: 'offset',
      reason: '需要随机页面访问功能',
      recommendations: [
        '实现缓存机制提高性能',
        '考虑使用覆盖索引优化查询'
      ]
    };
  }
  
  return {
    strategy: 'cursor',
    reason: '默认推荐游标分页以获得更好性能',
    recommendations: [
      '使用稳定的排序字段',
      '限制每页最大项目数',
      '提供清晰的文档说明'
    ]
  };
};

// 使用示例
const datasetAnalysis = {
  totalSize: 50000,
  updateFrequency: 'high',
  accessPattern: 'sequential',
  consistencyRequirements: 'high',
  performanceNeeds: 'low_latency'
};

const strategy = choosePaginationStrategy(datasetAnalysis);
console.log(`推荐分页策略: ${strategy.strategy}`);

6. 幂等性设计

6.1 幂等性原理与重要性

幂等性是分布式系统中保证数据一致性的关键概念,确保同一请求多次执行与单次执行效果相同。

幂等性使用场景

  • 支付处理系统
  • 订单创建和更新
  • 资源分配操作
  • 任何可能重试的写操作

6.2 幂等性实现模式

::: tabs

@tab 基于令牌的幂等性

// 幂等性令牌服务
class IdempotencyService {
  constructor(redisClient) {
    this.redis = redisClient;
    this.defaultTtl = 24 * 60 * 60; // 24小时
  }
  
  // 生成幂等性密钥
  generateKey() {
    return require('crypto').randomUUID();
  }
  
  // 检查并记录请求
  async checkAndRecord(key, requestHash, ttl = this.defaultTtl) {
    const redisKey = `idempotency:${key}`;
    
    // 使用Redis事务确保原子性
    const multi = this.redis.multi();
    
    // 检查是否已存在
    multi.get(redisKey);
    
    // 设置新键(如果不存在)
    multi.setnx(redisKey, JSON.stringify({
      status: 'processing',
      request_hash: requestHash,
      created_at: new Date().toISOString()
    }));
    
    // 设置过期时间
    multi.expire(redisKey, ttl);
    
    const results = await multi.exec();
    const existingRecord = results[0][1];
    
    if (existingRecord) {
      const record = JSON.parse(existingRecord);
      
      // 检查请求哈希是否匹配
      if (record.request_hash !== requestHash) {
        throw new Error('幂等性密钥已用于不同请求');
      }
      
      return { exists: true, record };
    }
    
    return { exists: false };
  }
  
  // 保存成功响应
  async saveResponse(key, response, statusCode, ttl = this.defaultTtl) {
    const redisKey = `idempotency:${key}`;
    const record = {
      status: 'completed',
      response: response,
      status_code: statusCode,
      completed_at: new Date().toISOString()
    };
    
    await this.redis.setex(redisKey, ttl, JSON.stringify(record));
  }
  
  // 保存错误信息
  async saveError(key, error, ttl = this.defaultTtl) {
    const redisKey = `idempotency:${key}`;
    const record = {
      status: 'error',
      error: error.message,
      failed_at: new Date().toISOString()
    };
    
    await this.redis.setex(redisKey, ttl, JSON.stringify(record));
  }
}

// 幂等性中间件
const idempotencyMiddleware = (redisClient) => {
  const idempotencyService = new IdempotencyService(redisClient);
  
  return async (req, res, next) => {
    // 只对幂等方法应用幂等性
    const idempotentMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
    if (!idempotentMethods.includes(req.method)) {
      return next();
    }
    
    const idempotencyKey = req.get('Idempotency-Key');
    if (!idempotencyKey) {
      return next();
    }
    
    // 生成请求哈希(排除某些可能变化的头信息)
    const requestHash = createRequestHash(req);
    
    try {
      const { exists, record } = await idempotencyService.checkAndRecord(
        idempotencyKey, 
        requestHash
      );
      
      if (exists) {
        if (record.status === 'completed') {
          // 返回缓存的成功响应
          return res.status(record.status_code).json(record.response);
        } else if (record.status === 'error') {
          // 返回缓存的错误
          return res.status(500).json({ error: record.error });
        } else if (record.status === 'processing') {
          // 请求正在处理中,返回重试提示
          return res.status(409).json({ 
            error: '请求正在处理中,请稍后重试',
            retry_after: 5 // 5秒后重试
          });
        }
      }
      
      // 存储原始send方法
      const originalSend = res.send;
      const originalJson = res.json;
      
      // 拦截响应以缓存结果
      res.json = function(body) {
        idempotencyService.saveResponse(idempotencyKey, body, res.statusCode)
          .catch(console.error);
        return originalJson.call(this, body);
      };
      
      res.send = function(body) {
        idempotencyService.saveResponse(idempotencyKey, body, res.statusCode)
          .catch(console.error);
        return originalSend.call(this, body);
      };
      
      next();
    } catch (error) {
      if (error.message === '幂等性密钥已用于不同请求') {
        return res.status(422).json({ 
          error: '幂等性密钥已用于不同请求内容' 
        });
      }
      
      console.error('幂等性处理错误:', error);
      next(); // 出错时继续处理请求
    }
  };
};

// 创建请求哈希
const createRequestHash = (req) => {
  const hash = require('crypto').createHash('sha256');
  
  // 包含方法、路径和请求体
  hash.update(req.method);
  hash.update(req.path);
  hash.update(JSON.stringify(req.body || {}));
  
  return hash.digest('hex');
};

@tab 数据库约束幂等性

// 基于数据库唯一约束的幂等性
class DatabaseIdempotency {
  constructor(db) {
    this.db = db;
  }
  
  async ensureIdempotencyTable() {
    await this.db.schema.createTableIfNotExists('idempotency_keys', (table) => {
      table.string('key').primary();
      table.string('request_hash').notNullable();
      table.string('resource_type').notNullable();
      table.string('resource_id').nullable();
      table.string('status').notNullable();
      table.json('response').nullable();
      table.timestamp('created_at').defaultTo(this.db.fn.now());
      table.timestamp('updated_at').defaultTo(this.db.fn.now());
    });
  }
  
  async processWithIdempotency(key, requestHash, resourceType, operation) {
    return await this.db.transaction(async (trx) => {
      try {
        // 检查现有记录
        const existing = await trx('idempotency_keys')
          .where('key', key)
          .first();
        
        if (existing) {
          if (existing.request_hash !== requestHash) {
            throw new Error('IDEMPOTENCY_KEY_CONFLICT');
          }
          
          if (existing.status === 'completed') {
            return {
              idempotent: true,
              response: existing.response
            };
          }
          
          throw new Error('REQUEST_IN_PROGRESS');
        }
        
        // 插入新记录
        await trx('idempotency_keys').insert({
          key,
          request_hash: requestHash,
          resource_type: resourceType,
          status: 'processing'
        });
        
        // 执行实际操作
        const result = await operation(trx);
        
        // 更新记录状态
        await trx('idempotency_keys')
          .where('key', key)
          .update({
            status: 'completed',
            response: result,
            resource_id: result.id,
            updated_at: this.db.fn.now()
          });
        
        return {
          idempotent: false,
          response: result
        };
        
      } catch (error) {
        await trx('idempotency_keys')
          .where('key', key)
          .update({
            status: 'error',
            error: error.message,
            updated_at: this.db.fn.now()
          });
        
        throw error;
      }
    });
  }
}

// 使用示例
app.post('/payments', async (req, res) => {
  const idempotencyKey = req.get('Idempotency-Key');
  const requestHash = createRequestHash(req);
  
  try {
    const dbIdempotency = new DatabaseIdempotency(db);
    
    const result = await dbIdempotency.processWithIdempotency(
      idempotencyKey,
      requestHash,
      'payment',
      async (trx) => {
        // 实际的支付处理逻辑
        const payment = await processPayment(req.body, trx);
        return payment;
      }
    );
    
    if (result.idempotent) {
      res.status(200).json(result.response);
    } else {
      res.status(201).json(result.response);
    }
    
  } catch (error) {
    if (error.message === 'IDEMPOTENCY_KEY_CONFLICT') {
      res.status(422).json({ error: '幂等性密钥冲突' });
    } else if (error.message === 'REQUEST_IN_PROGRESS') {
      res.status(409).json({ error: '请求处理中' });
    } else {
      res.status(500).json({ error: '支付处理失败' });
    }
  }
});

:::

6.3 幂等性最佳实践

// 幂等性配置管理
class IdempotencyConfig {
  constructor() {
    this.config = {
      // 默认TTL(秒)
      defaultTtl: 86400, // 24小时
      
      // 不同资源的TTL配置
      resourceTtl: {
        payment: 172800, // 48小时
        order: 259200,   // 72小时
        user: 31536000   // 1年
      },
      
      // 需要幂等性的端点
      endpoints: {
        '/api/payments': {
          required: true,
          methods: ['POST', 'PUT'],
          ttl: 172800
        },
        '/api/orders': {
          required: true,
          methods: ['POST'],
          ttl: 259200
        },
        '/api/users': {
          required: false,
          methods: ['POST']
        }
      },
      
      // 密钥格式验证
      keyValidation: {
        pattern: /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i,
        maxLength: 36
      }
    };
  }
  
  // 验证幂等性密钥格式
  validateKey(key) {
    if (!key) return { valid: false, reason: '密钥不能为空' };
    if (key.length > this.config.keyValidation.maxLength) {
      return { valid: false, reason: '密钥长度超出限制' };
    }
    if (!this.config.keyValidation.pattern.test(key)) {
      return { valid: false, reason: '密钥格式无效' };
    }
    return { valid: true };
  }
  
  // 获取资源的TTL
  getTtlForResource(resourceType) {
    return this.config.resourceTtl[resourceType] || this.config.defaultTtl;
  }
  
  // 检查端点是否需要幂等性
  isIdempotencyRequired(path, method) {
    const endpointConfig = this.config.endpoints[path];
    return endpointConfig && 
           endpointConfig.required && 
           endpointConfig.methods.includes(method);
  }
}

// 完整的幂等性处理流程
const completeIdempotencyHandler = (req, res, next) => {
  const config = new IdempotencyConfig();
  
  // 检查是否需要幂等性处理
  if (!config.isIdempotencyRequired(req.path, req.method)) {
    return next();
  }
  
  const idempotencyKey = req.get('Idempotency-Key');
  
  // 验证密钥格式
  const validation = config.validateKey(idempotencyKey);
  if (!validation.valid) {
    return res.status(400).json({ 
      error: `无效的幂等性密钥: ${validation.reason}` 
    });
  }
  
  // 实际的幂等性处理逻辑
  // ...(使用前面展示的令牌或数据库实现)
  
  next();
};

7. 过滤与排序

7.1 高级过滤实现

过滤功能让客户端能够精确获取所需数据,减少不必要的数据传输。

// 高级过滤解析器
class AdvancedFilterParser {
  constructor(allowedFilters) {
    this.allowedFilters = allowedFilters;
    this.operators = {
      eq: '=',
      ne: '!=',
      gt: '>',
      gte: '>=',
      lt: '<',
      lte: '<=',
      like: 'LIKE',
      in: 'IN',
      between: 'BETWEEN'
    };
  }
  
  // 解析查询参数
  parse(queryParams) {
    const filters = {};
    
    for (const [key, value] of Object.entries(queryParams)) {
      if (!this.allowedFilters.includes(key)) continue;
      
      // 支持多种格式:field[operator]=value
      const match = key.match(/^(\w+)(?:\[(\w+)\])?$/);
      if (!match) continue;
      
      const field = match[1];
      const operator = match[2] || 'eq';
      
      if (!this.operators[operator]) {
        throw new Error(`不支持的运算符: ${operator}`);
      }
      
      if (!filters[field]) {
        filters[field] = [];
      }
      
      filters[field].push({
        operator: this.operators[operator],
        value: this.parseValue(value, operator)
      });
    }
    
    return filters;
  }
  
  // 解析值(根据运算符处理不同类型)
  parseValue(value, operator) {
    switch (operator) {
      case 'in':
        return Array.isArray(value) ? value : value.split(',');
      
      case 'between':
        const values = Array.isArray(value) ? value : value.split(',');
        if (values.length !== 2) {
          throw new Error('BETWEEN操作需要两个值');
        }
        return values;
      
      case 'like':
        return `%${value}%`;
      
      default:
        return value;
    }
  }
  
  // 构建数据库查询
  buildQuery(queryBuilder, filters) {
    for (const [field, conditions] of Object.entries(filters)) {
      conditions.forEach(condition => {
        const { operator, value } = condition;
        
        switch (operator) {
          case 'IN':
            queryBuilder.whereIn(field, value);
            break;
          
          case 'BETWEEN':
            queryBuilder.whereBetween(field, value);
            break;
          
          case 'LIKE':
            queryBuilder.where(field, 'LIKE', value);
            break;
          
          default:
            queryBuilder.where(field, operator, value);
        }
      });
    }
    
    return queryBuilder;
  }
}

// 使用示例
const allowedFilters = ['name', 'email', 'age', 'created_at', 'status'];
const filterParser = new AdvancedFilterParser(allowedFilters);

app.get('/users/advanced', async (req, res) => {
  try {
    const filters = filterParser.parse(req.query);
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    
    let query = UserModel.query();
    
    // 应用过滤
    filterParser.buildQuery(query, filters);
    
    // 应用分页
    const result = await query
      .page(page - 1, limit)
      .orderBy('created_at', 'desc');
    
    res.json({
      data: result.results,
      pagination: {
        page,
        limit,
        total: result.total,
        pages: Math.ceil(result.total / limit)
      },
      filters: filters
    });
    
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

7.2 智能排序实现

排序功能让客户端能够以有意义的方式组织数据。

// 高级排序解析器
class AdvancedSortParser {
  constructor(allowedSortFields, defaultSort) {
    this.allowedSortFields = allowedSortFields;
    this.defaultSort = defaultSort;
  }
  
  // 解析排序参数
  parse(sortParam) {
    if (!sortParam) return [this.defaultSort];
    
    const sortRules = [];
    const sortItems = Array.isArray(sortParam) ? sortParam : sortParam.split(',');
    
    for (const item of sortItems) {
      let field = item;
      let direction = 'asc';
      
      // 支持格式:field:desc, field:asc
      if (item.includes(':')) {
        [field, direction] = item.split(':');
      }
      
      // 验证字段是否允许排序
      if (!this.allowedSortFields.includes(field)) {
        throw new Error(`不允许排序的字段: ${field}`);
      }
      
      // 验证排序方向
      if (direction !== 'asc' && direction !== 'desc') {
        throw new Error(`无效的排序方向: ${direction}`);
      }
      
      sortRules.push({ field, direction });
    }
    
    return sortRules.length > 0 ? sortRules : [this.defaultSort];
  }
  
  // 构建排序查询
  buildQuery(queryBuilder, sortRules) {
    sortRules.forEach(({ field, direction }) => {
      queryBuilder.orderBy(field, direction);
    });
    
    return queryBuilder;
  }
}

// 使用示例
const allowedSortFields = ['name', 'email', 'age', 'created_at', 'updated_at'];
const defaultSort = { field: 'created_at', direction: 'desc' };
const sortParser = new AdvancedSortParser(allowedSortFields, defaultSort);

// 在路由中使用
app.get('/users/sorted', async (req, res) => {
  try {
    const sortRules = sortParser.parse(req.query.sort);
    const filters = filterParser.parse(req.query);
    
    let query = UserModel.query();
    
    // 应用过滤和排序
    filterParser.buildQuery(query, filters);
    sortParser.buildQuery(query, sortRules);
    
    const result = await query;
    
    res.json({
      data: result,
      sorting: sortRules,
      filters: filters
    });
    
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

7.3 字段选择与性能优化

字段选择允许客户端指定需要返回的字段,减少不必要的数据传输。

// 字段选择器
class FieldSelector {
  constructor(allowedFields, defaultFields) {
    this.allowedFields = allowedFields;
    this.defaultFields = defaultFields;
  }
  
  // 解析字段选择参数
  parse(fieldsParam) {
    if (!fieldsParam) return this.defaultFields;
    
    const requestedFields = Array.isArray(fieldsParam) ? 
      fieldsParam : fieldsParam.split(',');
    
    // 验证字段是否允许
    const invalidFields = requestedFields.filter(
      field => !this.allowedFields.includes(field)
    );
    
    if (invalidFields.length > 0) {
      throw new Error(`不允许的字段: ${invalidFields.join(', ')}`);
    }
    
    return requestedFields;
  }
  
  // 构建选择查询
  buildQuery(queryBuilder, fields) {
    return queryBuilder.select(fields);
  }
}

// 完整的查询构建器
class QueryBuilder {
  constructor() {
    this.filterParser = new AdvancedFilterParser(['name', 'email', 'age', 'status']);
    this.sortParser = new AdvancedSortParser(['name', 'created_at'], 
      { field: 'created_at', direction: 'desc' });
    this.fieldSelector = new FieldSelector(
      ['id', 'name', 'email', 'age', 'created_at'],
      ['id', 'name', 'email']
    );
  }
  
  // 构建完整查询
  build(req) {
    return async (model) => {
      let query = model.query();
      
      try {
        // 解析查询参数
        const filters = this.filterParser.parse(req.query);
        const sortRules = this.sortParser.parse(req.query.sort);
        const fields = this.fieldSelector.parse(req.query.fields);
        
        // 应用过滤、排序和字段选择
        this.filterParser.buildQuery(query, filters);
        this.sortParser.buildQuery(query, sortRules);
        this.fieldSelector.buildQuery(query, fields);
        
        return query;
        
      } catch (error) {
        throw error;
      }
    };
  }
}

// 使用完整查询构建器
app.get('/users/optimized', async (req, res) => {
  try {
    const queryBuilder = new QueryBuilder();
    const query = await queryBuilder.build(req)(UserModel);
    
    const result = await query;
    
    res.json({
      data: result,
      meta: {
        count: result.length,
        // 包含使用的过滤、排序和字段信息
        query: {
          filters: queryBuilder.filterParser.parse(req.query),
          sorting: queryBuilder.sortParser.parse(req.query.sort),
          fields: queryBuilder.fieldSelector.parse(req.query.fields)
        }
      }
    });
    
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

总结

  1. REST基础原理:采用资源导向的设计理念,充分利用HTTP协议的语义化特性,构建可预测、易理解的API接口
  2. 错误处理机制:标准化错误响应格式,合理使用HTTP状态码,提供清晰的问题诊断信息
  3. API版本控制:通过多种版本控制策略确保向后兼容性,平滑处理API演进和破坏性变更
  4. 速率限制:采用分层限制策略保护系统资源,防止滥用同时保证合法用户的访问体验
  5. 分页技术:根据数据集特性选择合适的

原文:xuanhu.info/projects/it…

❌
❌