- Published on
RESTful API 完全指南
- Authors
- Name
- Yvan Yang
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 架构约束
统一接口约束:
- 资源标识
- 通过表述对资源进行操作
- 自描述消息
- 超媒体作为应用状态引擎(HATEOAS)
无状态约束:
- 服务器不存储客户端状态
- 每个请求包含所需的所有信息
- 提高可见性、可靠性和可扩展性
缓存约束:
- 响应必须隐式或显式标记为可缓存或不可缓存
- 提高客户端性能
- 提高服务器可扩展性
客户端-服务器约束:
- 关注点分离
- 提高用户界面可移植性
- 提高服务器组件可扩展性
分层系统约束:
- 允许架构由分层的层次结构组成
- 提高系统可扩展性
- 启用负载均衡和安全策略
2. RESTful API 设计原则
2.1 资源命名原则
- 使用名词表示资源:
✅ Good:
/users
/articles
/products
❌ Bad:
/getUsers
/createArticle
/deleteProduct
- 使用复数形式:
✅ Good:
/users
/articles
/products
❌ Bad:
/user
/article
/product
- 使用层级关系表示资源间的关系:
✅ Good:
/users/:id/posts
/organizations/:id/members
/products/:id/variants
❌ Bad:
/userPosts
/organizationMembers
/productVariants
2.2 HTTP 方法使用
- GET - 获取资源
GET /users // 获取用户列表
GET /users/:id // 获取特定用户
GET /users/:id/posts // 获取用户的所有文章
- POST - 创建资源
POST /users // 创建新用户
POST /articles // 创建新文章
- PUT - 完整更新资源
PUT /users/:id // 更新整个用户对象
PUT /articles/:id // 更新整个文章对象
- PATCH - 部分更新资源
PATCH /users/:id // 部分更新用户
PATCH /articles/:id // 部分更新文章
- DELETE - 删除资源
DELETE /users/:id // 删除用户
DELETE /articles/:id // 删除文章
2.3 状态码使用
- 2xx - 成功
200 OK - 请求成功
201 Created - 资源创建成功
204 No Content - 删除成功
- 4xx - 客户端错误
400 Bad Request - 请求格式错误
401 Unauthorized - 未认证
403 Forbidden - 无权限
404 Not Found - 资源不存在
409 Conflict - 资源冲突
429 Too Many Requests - 请求过多
- 5xx - 服务器错误
500 Internal Server Error - 服务器错误
502 Bad Gateway - 网关错误
503 Service Unavailable - 服务不可用
2.4 查询参数使用
- 过滤 (Filtering)
GET /products?category=electronics
GET /users?role=admin
- 排序 (Sorting)
GET /products?sort=price:desc
GET /users?sort=created_at:asc
- 分页 (Pagination)
GET /products?page=2&per_page=20
GET /users?offset=20&limit=10
- 字段选择 (Field Selection)
GET /users?fields=id,username,email
GET /products?fields=id,name,price
- 搜索 (Search)
GET /products?search=keyword
GET /users?q=john
2.5 响应格式
- 成功响应
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"username": "john_doe",
"email": "john@example.com"
}
},
"meta": {
"timestamp": "2024-01-16T10:00:00Z"
}
}
- 错误响应
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
},
"meta": {
"timestamp": "2024-01-16T10:00:00Z"
}
}
- 集合响应
{
"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 版本控制
- URL 版本控制
/api/v1/users
/api/v2/users
- Header 版本控制
Accept: application/vnd.company.api+json;version=1
3.2 认证与授权
- 使用 Bearer Token
Authorization: Bearer <token>
- OAuth2 流程
/oauth/token
/oauth/authorize
3.3 速率限制
- 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 框架,包含:
统一的响应格式:
- 成功响应包含 data 和 meta
- 错误响应包含 error 和 meta
- HATEOAS 链接支持
缓存管理:
- ETag 支持
- Cache-Control 头
- 条件请求处理
安全特性:
- CORS 配置
- 速率限制
- 认证和授权
性能优化:
- 压缩支持
- Redis 缓存
- 响应格式化
API 版本控制:
- URL 版本控制 (/api/v1/...)
- 可扩展的路由结构
最佳实践:
- 控制器基类
- 中间件模块化
- 服务层分离