y.y
Published on

RESTful API 完全指南

Authors

RESTful API 完全指南

1. REST 基本概念

1.1 什么是 REST

REST (Representational State Transfer) 是由 Roy Fielding 在其 2000 年的博士论文中提出的软件架构风格。它是一种基于以下核心原则的分布式系统架构:

  • 统一接口 (Uniform Interface)
  • 无状态 (Stateless)
  • 可缓存 (Cacheable)
  • 客户端-服务器分离 (Client-Server)
  • 分层系统 (Layered System)
  • 按需代码(可选)(Code on Demand)

1.2 REST 架构约束

  1. 统一接口约束

    • 资源标识
    • 通过表述对资源进行操作
    • 自描述消息
    • 超媒体作为应用状态引擎(HATEOAS)
  2. 无状态约束

    • 服务器不存储客户端状态
    • 每个请求包含所需的所有信息
    • 提高可见性、可靠性和可扩展性
  3. 缓存约束

    • 响应必须隐式或显式标记为可缓存或不可缓存
    • 提高客户端性能
    • 提高服务器可扩展性
  4. 客户端-服务器约束

    • 关注点分离
    • 提高用户界面可移植性
    • 提高服务器组件可扩展性
  5. 分层系统约束

    • 允许架构由分层的层次结构组成
    • 提高系统可扩展性
    • 启用负载均衡和安全策略

2. RESTful API 设计原则

2.1 资源命名原则

  1. 使用名词表示资源
Good:
/users
/articles
/products

Bad:
/getUsers
/createArticle
/deleteProduct
  1. 使用复数形式
Good:
/users
/articles
/products

Bad:
/user
/article
/product
  1. 使用层级关系表示资源间的关系
Good:
/users/:id/posts
/organizations/:id/members
/products/:id/variants

Bad:
/userPosts
/organizationMembers
/productVariants

2.2 HTTP 方法使用

  1. GET - 获取资源
GET /users          // 获取用户列表
GET /users/:id      // 获取特定用户
GET /users/:id/posts // 获取用户的所有文章
  1. POST - 创建资源
POST /users         // 创建新用户
POST /articles      // 创建新文章
  1. PUT - 完整更新资源
PUT /users/:id      // 更新整个用户对象
PUT /articles/:id   // 更新整个文章对象
  1. PATCH - 部分更新资源
PATCH /users/:id    // 部分更新用户
PATCH /articles/:id // 部分更新文章
  1. DELETE - 删除资源
DELETE /users/:id   // 删除用户
DELETE /articles/:id // 删除文章

2.3 状态码使用

  1. 2xx - 成功
200 OK - 请求成功
201 Created - 资源创建成功
204 No Content - 删除成功
  1. 4xx - 客户端错误
400 Bad Request - 请求格式错误
401 Unauthorized - 未认证
403 Forbidden - 无权限
404 Not Found - 资源不存在
409 Conflict - 资源冲突
429 Too Many Requests - 请求过多
  1. 5xx - 服务器错误
500 Internal Server Error - 服务器错误
502 Bad Gateway - 网关错误
503 Service Unavailable - 服务不可用

2.4 查询参数使用

  1. 过滤 (Filtering)
GET /products?category=electronics
GET /users?role=admin
  1. 排序 (Sorting)
GET /products?sort=price:desc
GET /users?sort=created_at:asc
  1. 分页 (Pagination)
GET /products?page=2&per_page=20
GET /users?offset=20&limit=10
  1. 字段选择 (Field Selection)
GET /users?fields=id,username,email
GET /products?fields=id,name,price
  1. 搜索 (Search)
GET /products?search=keyword
GET /users?q=john

2.5 响应格式

  1. 成功响应
{
  "data": {
    "id": "123",
    "type": "users",
    "attributes": {
      "username": "john_doe",
      "email": "john@example.com"
    }
  },
  "meta": {
    "timestamp": "2024-01-16T10:00:00Z"
  }
}
  1. 错误响应
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ]
  },
  "meta": {
    "timestamp": "2024-01-16T10:00:00Z"
  }
}
  1. 集合响应
{
  "data": [
    {
      "id": "123",
      "type": "users",
      "attributes": {
        "username": "john_doe"
      }
    }
  ],
  "meta": {
    "total": 100,
    "page": 1,
    "per_page": 20
  },
  "links": {
    "self": "/api/users?page=1",
    "next": "/api/users?page=2",
    "prev": null
  }
}

3. 实现最佳实践

3.1 版本控制

  1. URL 版本控制
/api/v1/users
/api/v2/users
  1. Header 版本控制
Accept: application/vnd.company.api+json;version=1

3.2 认证与授权

  1. 使用 Bearer Token
Authorization: Bearer <token>
  1. OAuth2 流程
/oauth/token
/oauth/authorize

3.3 速率限制

  1. Header 信息
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1642328400

3.4 HATEOAS

{
  "data": {
    "id": "123",
    "type": "orders",
    "attributes": {
      "status": "pending"
    },
    "links": {
      "self": "/api/orders/123",
      "cancel": "/api/orders/123/cancel",
      "pay": "/api/orders/123/pay"
    }
  }
}

4. 安全最佳实践

4.1 通信安全

  • 使用 HTTPS
  • 实施 CORS 策略
  • 设置安全 Headers

4.2 认证和授权

  • 使用 JWT
  • 实施 OAuth2
  • 角色基础访问控制

4.3 输入验证

  • 验证所有输入
  • 防止 SQL 注入
  • 防止 XSS 攻击

5. 性能优化

5.1 缓存策略

Cache-Control: public, max-age=31536000
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

5.2 压缩

Accept-Encoding: gzip, deflate
Content-Encoding: gzip

5.3 条件请求

If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Sat, 29 Oct 2024 19:43:31 GMT

6. 文档化

6.1 OpenAPI 规范

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get users list
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Success

具体实现示例

// src/types/api.ts
export interface ApiResponse<T> {
  data: T;
  meta?: Record<string, unknown>;
  links?: Record<string, string | null>;
}

export interface ApiError {
  error: {
    code: string;
    message: string;
    details?: unknown;
  };
  meta?: {
    timestamp: string;
    request_id: string;
  };
}

// src/middleware/response.ts
import { Request, Response, NextFunction } from 'express';

export function formatResponse(req: Request, res: Response, next: NextFunction): void {
  // 扩展 response 对象
  const originalJson = res.json;
  const originalSend = res.send;

  // 重写 json 方法
  res.json = function(body: unknown): Response {
    if (res.statusCode >= 400) {
      return originalJson.call(this, {
        error: {
          code: body.code || 'UNKNOWN_ERROR',
          message: body.message || 'An unknown error occurred',
          details: body.details,
        },
        meta: {
          timestamp: new Date().toISOString(),
          request_id: req.id,
        },
      });
    }

    return originalJson.call(this, {
      data: body,
      meta: {
        timestamp: new Date().toISOString(),
        request_id: req.id,
      },
    });
  };

  // 重写 send 方法以处理非 JSON 响应
  res.send = function(body: unknown): Response {
    if (typeof body === 'string') {
      return originalSend.call(this, body);
    }
    return res.json(body);
  };

  next();
}

// src/controllers/base.controller.ts
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import { ApiError } from '../types/api';

export abstract class BaseController {
  protected async handleRequest<T>(
    req: Request,
    res: Response,
    next: NextFunction,
    handler: () => Promise<T>
  ): Promise<void> {
    try {
      const result = await handler();
      res.json(result);
    } catch (error) {
      next(error);
    }
  }

  protected validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
    try {
      return schema.parse(data);
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw {
          code: 'VALIDATION_ERROR',
          message: 'Validation failed',
          details: error.errors,
        } as ApiError;
      }
      throw error;
    }
  }

  protected getPaginationParams(req: Request): { page: number; per_page: number } {
    const page = parseInt(req.query.page as string) || 1;
    const per_page = parseInt(req.query.per_page as string) || 20;
    return { page, per_page };
  }

  protected getSortParams(req: Request): { field: string; order: 'asc' | 'desc' } {
    const sort = (req.query.sort as string) || '';
    const [field, order] = sort.split(':');
    return {
      field: field || 'created_at',
      order: (order as 'asc' | 'desc') || 'desc',
    };
  }

  protected getFieldSelection(req: Request): string[] {
    const fields = (req.query.fields as string) || '';
    return fields.split(',').filter(Boolean);
  }
}

// src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { BaseController } from './base.controller';
import { UserService } from '../services/user.service';
import { createUserSchema, updateUserSchema } from '../schemas/user.schema';

export class UserController extends BaseController {
  constructor(private userService: UserService) {
    super();
  }

  public async getUsers(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { page, per_page } = this.getPaginationParams(req);
      const { field, order } = this.getSortParams(req);
      const fields = this.getFieldSelection(req);

      const users = await this.userService.list({
        page,
        per_page,
        sort: { field, order },
        fields,
      });

      // Add HATEOAS links
      const totalPages = Math.ceil(users.total / per_page);
      const links = {
        self: `/api/v1/users?page=${page}&per_page=${per_page}`,
        first: `/api/v1/users?page=1&per_page=${per_page}`,
        last: `/api/v1/users?page=${totalPages}&per_page=${per_page}`,
        next: page < totalPages ? `/api/v1/users?page=${page + 1}&per_page=${per_page}` : null,
        prev: page > 1 ? `/api/v1/users?page=${page - 1}&per_page=${per_page}` : null,
      };

      return {
        ...users,
        links,
      };
    });
  }

  public async createUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const data = this.validate(createUserSchema, req.body);
      const user = await this.userService.create(data);
      res.status(201);
      return user;
    });
  }

  // src/controllers/user.controller.ts (continued)
  public async getUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      const fields = this.getFieldSelection(req);
      const user = await this.userService.findById(id, fields);

      // Add HATEOAS links
      return {
        ...user,
        links: {
          self: `/api/v1/users/${id}`,
          posts: `/api/v1/users/${id}/posts`,
          update: `/api/v1/users/${id}`,
          delete: `/api/v1/users/${id}`
        }
      };
    });
  }

  public async updateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      const data = this.validate(updateUserSchema, req.body);
      
      // 检查 If-Match 头以实现乐观锁
      const etag = req.header('If-Match');
      if (etag) {
        await this.userService.checkVersion(id, etag);
      }

      const user = await this.userService.update(id, data);
      return user;
    });
  }

  public async deleteUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      await this.userService.delete(id);
      res.status(204).send();
    });
  }
}

// src/middleware/cache.ts
import { Request, Response, NextFunction } from 'express';
import { createHash } from 'crypto';

export function cacheControl(options: {
  public?: boolean;
  maxAge?: number;
  staleWhileRevalidate?: number;
}) {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (req.method === 'GET') {
      const directives = [];
      
      if (options.public) {
        directives.push('public');
      } else {
        directives.push('private');
      }

      if (options.maxAge) {
        directives.push(`max-age=${options.maxAge}`);
      }

      if (options.staleWhileRevalidate) {
        directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
      }

      res.set('Cache-Control', directives.join(', '));
    }
    next();
  };
}

export function etag() {
  return (req: Request, res: Response, next: NextFunction): void => {
    const originalSend = res.send;

    res.send = function(body: any): Response {
      if (req.method === 'GET') {
        const content = typeof body === 'string' ? body : JSON.stringify(body);
        const etag = createHash('sha256').update(content).digest('hex');
        
        res.set('ETag', `"${etag}"`);

        if (req.fresh) {
          res.status(304).send();
          return res;
        }
      }
      
      return originalSend.call(this, body);
    };

    next();
  };
}

// src/routes/user.routes.ts
import express from 'express';
import { UserController } from '../controllers/user.controller';
import { authenticate, authorize } from '../middleware/auth';
import { cacheControl, etag } from '../middleware/cache';
import { validateRequest } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../schemas/user.schema';

export function createUserRouter(controller: UserController): express.Router {
  const router = express.Router();

  router.route('/')
    .get(
      authenticate,
      cacheControl({ public: true, maxAge: 300 }), // 5分钟缓存
      etag(),
      controller.getUsers.bind(controller)
    )
    .post(
      authenticate,
      authorize('admin'),
      validateRequest(createUserSchema),
      controller.createUser.bind(controller)
    );

  router.route('/:id')
    .get(
      authenticate,
      cacheControl({ public: true, maxAge: 300 }),
      etag(),
      controller.getUser.bind(controller)
    )
    .patch(
      authenticate,
      validateRequest(updateUserSchema),
      controller.updateUser.bind(controller)
    )
    .delete(
      authenticate,
      authorize('admin'),
      controller.deleteUser.bind(controller)
    );

  return router;
}

// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
import { Redis } from 'ioredis';
import { Request, Response } from 'express';

export function createRateLimiter(redis: Redis) {
  return rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 限制每个IP 100次请求
    standardHeaders: true,
    legacyHeaders: false,
    keyGenerator: (req: Request): string => {
      // 使用 API key 或 IP 作为限制标识
      return req.header('X-API-Key') || req.ip;
    },
    handler: (req: Request, res: Response): void => {
      res.status(429).json({
        error: {
          code: 'RATE_LIMIT_EXCEEDED',
          message: 'Too many requests, please try again later.',
          details: {
            retryAfter: res.getHeader('Retry-After'),
          },
        },
      });
    },
    store: {
      init: () => Promise.resolve(),
      increment: async (key: string) => {
        const count = await redis.incr(key);
        await redis.expire(key, 900); // 15分钟
        return count;
      },
      decrement: (key: string) => redis.decr(key),
      resetKey: (key: string) => redis.del(key),
    },
  });
}

// src/middleware/cors.ts
import cors from 'cors';
import { Request } from 'express';

export const corsOptions: cors.CorsOptions = {
  origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
    const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [];
    
    // 允许没有 origin 的请求(比如 curl)
    if (!origin) {
      callback(null, true);
      return;
    }

    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key'],
  exposedHeaders: ['ETag', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
  credentials: true,
  maxAge: 86400, // 24小时
};

// src/app.ts
import express from 'express';
import helmet from 'helmet';
import compression from 'compression';
import { createRateLimiter } from './middleware/rateLimit';
import { corsOptions } from './middleware/cors';
import { formatResponse } from './middleware/response';
import { errorHandler } from './middleware/error';
import { createUserRouter } from './routes/user.routes';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { Redis } from 'ioredis';

export function createApp(redis: Redis): express.Application {
  const app = express();

  // 基础中间件
  app.use(helmet());
  app.use(cors(corsOptions));
  app.use(compression());
  app.use(express.json());

  // 请求限制
  app.use(createRateLimiter(redis));

  // 响应格式化
  app.use(formatResponse);

  // 路由
  const userService = new UserService();
  const userController = new UserController(userService);
  app.use('/api/v1/users', createUserRouter(userController));

  // 错误处理
  app.use(errorHandler);

  return app;
}

继续完成 RESTful API 的具体实现示例:

// src/controllers/user.controller.ts (continued)
  public async getUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      const fields = this.getFieldSelection(req);
      const user = await this.userService.findById(id, fields);

      // Add HATEOAS links
      return {
        ...user,
        links: {
          self: `/api/v1/users/${id}`,
          posts: `/api/v1/users/${id}/posts`,
          update: `/api/v1/users/${id}`,
          delete: `/api/v1/users/${id}`
        }
      };
    });
  }

  public async updateUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      const data = this.validate(updateUserSchema, req.body);
      
      // 检查 If-Match 头以实现乐观锁
      const etag = req.header('If-Match');
      if (etag) {
        await this.userService.checkVersion(id, etag);
      }

      const user = await this.userService.update(id, data);
      return user;
    });
  }

  public async deleteUser(req: Request, res: Response, next: NextFunction): Promise<void> {
    await this.handleRequest(req, res, next, async () => {
      const { id } = req.params;
      await this.userService.delete(id);
      res.status(204).send();
    });
  }
}

// src/middleware/cache.ts
import { Request, Response, NextFunction } from 'express';
import { createHash } from 'crypto';

export function cacheControl(options: {
  public?: boolean;
  maxAge?: number;
  staleWhileRevalidate?: number;
}) {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (req.method === 'GET') {
      const directives = [];
      
      if (options.public) {
        directives.push('public');
      } else {
        directives.push('private');
      }

      if (options.maxAge) {
        directives.push(`max-age=${options.maxAge}`);
      }

      if (options.staleWhileRevalidate) {
        directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
      }

      res.set('Cache-Control', directives.join(', '));
    }
    next();
  };
}

export function etag() {
  return (req: Request, res: Response, next: NextFunction): void => {
    const originalSend = res.send;

    res.send = function(body: any): Response {
      if (req.method === 'GET') {
        const content = typeof body === 'string' ? body : JSON.stringify(body);
        const etag = createHash('sha256').update(content).digest('hex');
        
        res.set('ETag', `"${etag}"`);

        if (req.fresh) {
          res.status(304).send();
          return res;
        }
      }
      
      return originalSend.call(this, body);
    };

    next();
  };
}

// src/routes/user.routes.ts
import express from 'express';
import { UserController } from '../controllers/user.controller';
import { authenticate, authorize } from '../middleware/auth';
import { cacheControl, etag } from '../middleware/cache';
import { validateRequest } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../schemas/user.schema';

export function createUserRouter(controller: UserController): express.Router {
  const router = express.Router();

  router.route('/')
    .get(
      authenticate,
      cacheControl({ public: true, maxAge: 300 }), // 5分钟缓存
      etag(),
      controller.getUsers.bind(controller)
    )
    .post(
      authenticate,
      authorize('admin'),
      validateRequest(createUserSchema),
      controller.createUser.bind(controller)
    );

  router.route('/:id')
    .get(
      authenticate,
      cacheControl({ public: true, maxAge: 300 }),
      etag(),
      controller.getUser.bind(controller)
    )
    .patch(
      authenticate,
      validateRequest(updateUserSchema),
      controller.updateUser.bind(controller)
    )
    .delete(
      authenticate,
      authorize('admin'),
      controller.deleteUser.bind(controller)
    );

  return router;
}

// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
import { Redis } from 'ioredis';
import { Request, Response } from 'express';

export function createRateLimiter(redis: Redis) {
  return rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 限制每个IP 100次请求
    standardHeaders: true,
    legacyHeaders: false,
    keyGenerator: (req: Request): string => {
      // 使用 API key 或 IP 作为限制标识
      return req.header('X-API-Key') || req.ip;
    },
    handler: (req: Request, res: Response): void => {
      res.status(429).json({
        error: {
          code: 'RATE_LIMIT_EXCEEDED',
          message: 'Too many requests, please try again later.',
          details: {
            retryAfter: res.getHeader('Retry-After'),
          },
        },
      });
    },
    store: {
      init: () => Promise.resolve(),
      increment: async (key: string) => {
        const count = await redis.incr(key);
        await redis.expire(key, 900); // 15分钟
        return count;
      },
      decrement: (key: string) => redis.decr(key),
      resetKey: (key: string) => redis.del(key),
    },
  });
}

// src/middleware/cors.ts
import cors from 'cors';
import { Request } from 'express';

export const corsOptions: cors.CorsOptions = {
  origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
    const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [];
    
    // 允许没有 origin 的请求(比如 curl)
    if (!origin) {
      callback(null, true);
      return;
    }

    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key'],
  exposedHeaders: ['ETag', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
  credentials: true,
  maxAge: 86400, // 24小时
};

// src/app.ts
import express from 'express';
import helmet from 'helmet';
import compression from 'compression';
import { createRateLimiter } from './middleware/rateLimit';
import { corsOptions } from './middleware/cors';
import { formatResponse } from './middleware/response';
import { errorHandler } from './middleware/error';
import { createUserRouter } from './routes/user.routes';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { Redis } from 'ioredis';

export function createApp(redis: Redis): express.Application {
  const app = express();

  // 基础中间件
  app.use(helmet());
  app.use(cors(corsOptions));
  app.use(compression());
  app.use(express.json());

  // 请求限制
  app.use(createRateLimiter(redis));

  // 响应格式化
  app.use(formatResponse);

  // 路由
  const userService = new UserService();
  const userController = new UserController(userService);
  app.use('/api/v1/users', createUserRouter(userController));

  // 错误处理
  app.use(errorHandler);

  return app;
}

这个实现提供了一个完整的 RESTful API 框架,包含:

  1. 统一的响应格式

    • 成功响应包含 data 和 meta
    • 错误响应包含 error 和 meta
    • HATEOAS 链接支持
  2. 缓存管理

    • ETag 支持
    • Cache-Control 头
    • 条件请求处理
  3. 安全特性

    • CORS 配置
    • 速率限制
    • 认证和授权
  4. 性能优化

    • 压缩支持
    • Redis 缓存
    • 响应格式化
  5. API 版本控制

    • URL 版本控制 (/api/v1/...)
    • 可扩展的路由结构
  6. 最佳实践

    • 控制器基类
    • 中间件模块化
    • 服务层分离