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 指定格式)
javascript
// 良好的资源设计示例
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 操作
javascript
// 用户资源完整的 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 架构的一个关键约束,它使客户端能够通过服务器提供的超媒体动态发现可用的操作。
javascript
// 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 错误响应格式标准化
一致的错误响应格式帮助客户端统一处理各种错误情况。
javascript
// 统一的错误处理中间件
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 安全性和稳定性的第一道防线。
javascript
// 使用 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 路径版本控制
javascript
// 版本控制中间件
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 头版本控制
javascript
// 基于 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 查询参数版本控制
javascript
// 不推荐的查询参数版本控制
app.get('/api/users', (req, res) => {
const version = req.query.version || '1';
if (version === '1') {
// v1 实现
} else if (version === '2') {
// v2 实现
}
});
:::
3.2 版本迁移策略
当引入破坏性变更时,需要制定周密的迁移计划。
3.3 版本管理最佳实践
javascript
// 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 令牌桶算法
javascript
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 滑动窗口算法
javascript
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 漏桶算法
javascript
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 多维度速率限制
在实际应用中,通常需要基于多个维度进行速率限制。
javascript
// 多维度速率限制配置
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 速率限制响应头
提供详细的速率限制信息帮助客户端合理调整请求频率。
javascript
// 速率限制响应头中间件
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 游标分页
分页是处理大型数据集的关键技术,不同的分页策略适用于不同的场景。
对比分析:
随机访问需求
简单实现] B --> D[大型数据集
顺序访问
性能要求高] 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 偏移分页实现
偏移分页是最传统和广泛使用的分页方法,适用于大多数中小型数据集。
javascript
// 偏移分页实现
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 游标分页实现
游标分页解决了偏移分页的性能和数据一致性问题,特别适合大型数据集和无限滚动场景。
javascript
// 游标分页实现
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 分页策略选择指南
选择合适的分页策略需要考虑多个因素:
javascript
// 分页策略选择函数
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 基于令牌的幂等性
javascript
// 幂等性令牌服务
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 数据库约束幂等性
javascript
// 基于数据库唯一约束的幂等性
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 幂等性最佳实践
javascript
// 幂等性配置管理
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 高级过滤实现
过滤功能让客户端能够精确获取所需数据,减少不必要的数据传输。
javascript
// 高级过滤解析器
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 智能排序实现
排序功能让客户端能够以有意义的方式组织数据。
javascript
// 高级排序解析器
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 字段选择与性能优化
字段选择允许客户端指定需要返回的字段,减少不必要的数据传输。
javascript
// 字段选择器
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 });
}
});
总结
- REST基础原理:采用资源导向的设计理念,充分利用HTTP协议的语义化特性,构建可预测、易理解的API接口
- 错误处理机制:标准化错误响应格式,合理使用HTTP状态码,提供清晰的问题诊断信息
- API版本控制:通过多种版本控制策略确保向后兼容性,平滑处理API演进和破坏性变更
- 速率限制:采用分层限制策略保护系统资源,防止滥用同时保证合法用户的访问体验
- 分页技术:根据数据集特性选择合适的