Chenyj Space
返回博客
·24 分钟

RESTful API 设计最佳实践

APIREST后端架构设计后端

REST 核心原则

什么是 REST

REST (Representational State Transfer) 是一种架构风格,用于设计网络应用程序的 API。

核心约束

  1. 客户端-服务器:分离关注点
  2. 无状态:每个请求包含所有必要信息
  3. 可缓存:响应必须明确表示是否可缓存
  4. 统一接口:标准化的接口设计
  5. 分层系统:客户端无需知道是否直接连接服务器

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 需要:

  1. 遵循 REST 原则:资源导向、无状态、统一接口
  2. 规范 URL 设计:名词、复数、嵌套关系
  3. 正确使用 HTTP 方法:GET/POST/PUT/PATCH/DELETE
  4. 统一响应格式:一致的成功/错误响应结构
  5. 完善的安全措施:认证、授权、限流
  6. 良好的缓存策略:ETag、Cache-Control
  7. 全面的错误处理:统一错误格式、有意义的错误信息
  8. 详细的 API 文档:Swagger/OpenAPI 规范

遵循这些最佳实践,可以构建出易用、可靠、可维护的 API 接口。