·24 分钟
RESTful API 设计最佳实践
APIREST后端架构设计后端
REST 核心原则
什么是 REST
REST (Representational State Transfer) 是一种架构风格,用于设计网络应用程序的 API。
核心约束:
- 客户端-服务器:分离关注点
- 无状态:每个请求包含所有必要信息
- 可缓存:响应必须明确表示是否可缓存
- 统一接口:标准化的接口设计
- 分层系统:客户端无需知道是否直接连接服务器
RESTful API 设计原则
code
❌ 不好的设计
GET /getUsers
POST /createUser
POST /updateUser
POST /deleteUser
✅ 好的设计
GET /users # 获取用户列表
POST /users # 创建用户
GET /users/:id # 获取单个用户
PUT /users/:id # 更新用户
DELETE /users/:id # 删除用户
URL 设计规范
资源命名
typescript
// ✅ 使用名词,复数形式
GET /articles # 文章列表
GET /articles/123 # 单篇文章
GET /users/456/articles # 用户的文章
// ❌ 避免动词
GET /getArticles
POST /createArticle
POST /updateArticle
// ✅ 使用嵌套表示关系
GET /users/123/orders # 用户的订单
GET /users/123/orders/456 # 用户的特定订单
GET /users/123/orders/456/items # 订单的商品
查询参数设计
typescript
// 分页
GET /articles?page=1&limit=20
// 排序
GET /articles?sort=createdAt:desc
GET /articles?sort=views:desc,createdAt:desc
// 过滤
GET /articles?category=tech&status=published
GET /articles?author=123&dateFrom=2024-01-01
// 搜索
GET /articles?q=javascript&fields=title,content
// 字段选择
GET /articles?fields=id,title,excerpt
HTTP 方法使用
方法语义
typescript
// GET - 获取资源,幂等
GET /articles # 获取列表
GET /articles/123 # 获取单个
GET /articles/123/comments # 获取关联资源
// POST - 创建资源,非幂等
POST /articles # 创建文章
POST /articles/123/comments # 创建评论
// PUT - 全量更新,幂等
PUT /articles/123 # 更新整个文章
// PATCH - 部分更新,幂等
PATCH /articles/123 # 更新部分字段
// DELETE - 删除资源,幂等
DELETE /articles/123 # 删除文章
幂等性设计
typescript
// ✅ 幂等操作 - 多次调用结果相同
GET /articles/123 # 读取,总是返回相同结果
PUT /articles/123 # 更新,设置相同值结果不变
DELETE /articles/123 # 删除,第二次调用返回 404
// ❌ 非幂等操作 - 多次调用结果不同
POST /articles # 每次创建新文章
POST /payments # 每次创建新支付
// 实现幂等性
import { v4 as uuidv4 } from 'uuid';
app.post('/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key required' });
}
// 检查是否已处理
const existing = await Payment.findByKey(idempotencyKey);
if (existing) {
return res.json(existing);
}
// 处理支付
const payment = await Payment.create({
...req.body,
idempotencyKey,
});
res.status(201).json(payment);
});
响应格式设计
统一响应结构
typescript
// 成功响应
{
"success": true,
"data": {
"id": "123",
"title": "文章标题",
"content": "文章内容"
},
"meta": {
"timestamp": "2024-01-01T00:00:00Z",
"requestId": "req_abc123"
}
}
// 列表响应
{
"success": true,
"data": [
{ "id": "1", "title": "文章1" },
{ "id": "2", "title": "文章2" }
],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
// 错误响应
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数无效",
"details": [
{
"field": "email",
"message": "邮箱格式不正确"
}
]
}
}
状态码使用
typescript
// 2xx 成功
200 OK # GET 成功
201 Created # POST 创建成功
204 No Content # DELETE 删除成功
// 3xx 重定向
301 Moved Permanently # 永久重定向
302 Found # 临时重定向
304 Not Modified # 缓存未修改
// 4xx 客户端错误
400 Bad Request # 请求参数错误
401 Unauthorized # 未认证
403 Forbidden # 无权限
404 Not Found # 资源不存在
405 Method Not Allowed # 方法不允许
409 Conflict # 资源冲突
422 Unprocessable Entity # 业务逻辑错误
429 Too Many Requests # 请求过于频繁
// 5xx 服务端错误
500 Internal Server Error # 服务器内部错误
502 Bad Gateway # 网关错误
503 Service Unavailable # 服务不可用
认证与授权
JWT 认证
typescript
// middleware/auth.ts
import jwt from 'jsonwebtoken';
export const authenticate = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// 使用
app.get('/protected', authenticate, (req, res) => {
res.json({ user: req.user });
});
RBAC 权限控制
typescript
// middleware/authorize.ts
export const authorize = (...roles: string[]) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// 使用
app.delete('/articles/:id',
authenticate,
authorize('admin', 'editor'),
deleteArticle
);
版本控制
URL 版本控制
typescript
// /api/v1/articles
// /api/v2/articles
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Header 版本控制
typescript
// Accept: application/vnd.api.v1+json
// Accept: application/vnd.api.v2+json
app.get('/articles', (req, res) => {
const version = req.accepts([
'application/vnd.api.v2+json',
'application/vnd.api.v1+json'
]);
if (version === 'application/vnd.api.v2+json') {
// v2 逻辑
} else {
// v1 逻辑
}
});
限流与缓存
限流实现
typescript
import rateLimit from 'express-rate-limit';
// 全局限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 最多 100 次请求
message: { error: 'Too many requests' },
});
app.use('/api/', limiter);
// 特定路由限流
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 登录尝试限制
skipSuccessfulRequests: true,
});
app.post('/auth/login', authLimiter, loginHandler);
缓存策略
typescript
// ETag 缓存
app.get('/articles/:id', async (req, res) => {
const article = await Article.findById(req.params.id);
const etag = generateETag(article);
res.set('ETag', etag);
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(article);
});
// Cache-Control
app.get('/articles', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600'); // 1小时
// ...
});
// 条件请求
app.get('/articles', (req, res) => {
const lastModified = await Article.getLastModified();
res.set('Last-Modified', lastModified.toUTCString());
if (req.headers['if-modified-since']) {
const ifModifiedSince = new Date(req.headers['if-modified-since']);
if (lastModified <= ifModifiedSince) {
return res.status(304).end();
}
}
// ...
});
错误处理
统一错误处理
typescript
// errors/AppError.ts
export class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: any
) {
super(message);
}
}
// 使用
throw new AppError(404, 'NOT_FOUND', '文章不存在');
throw new AppError(422, 'VALIDATION_ERROR', '参数错误', { field: 'email' });
// middleware/errorHandler.ts
export const errorHandler = (err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: {
code: err.code,
message: err.message,
details: err.details,
},
});
}
// 未知错误
console.error(err);
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: '服务器内部错误',
},
});
};
文档与测试
Swagger/OpenAPI 文档
yaml
# openapi.yaml
openapi: 3.0.0
info:
title: Blog API
version: 1.0.0
paths:
/articles:
get:
summary: 获取文章列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleList'
post:
summary: 创建文章
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleInput'
responses:
'201':
description: 创建成功
总结
设计高质量的 RESTful API 需要:
- 遵循 REST 原则:资源导向、无状态、统一接口
- 规范 URL 设计:名词、复数、嵌套关系
- 正确使用 HTTP 方法:GET/POST/PUT/PATCH/DELETE
- 统一响应格式:一致的成功/错误响应结构
- 完善的安全措施:认证、授权、限流
- 良好的缓存策略:ETag、Cache-Control
- 全面的错误处理:统一错误格式、有意义的错误信息
- 详细的 API 文档:Swagger/OpenAPI 规范
遵循这些最佳实践,可以构建出易用、可靠、可维护的 API 接口。