RESTful API 设计
问题
什么是 RESTful API?如何设计符合规范的 API 接口?
答案
RESTful API 是一种基于 REST(Representational State Transfer,表现层状态转换)架构风格的 API 设计规范,使用 HTTP 协议进行通信。
REST 核心原则
| 原则 | 说明 |
|---|---|
| 客户端-服务端分离 | 前后端独立演进 |
| 无状态 | 每次请求包含所有信息 |
| 可缓存 | 响应可标记为可缓存 |
| 统一接口 | 一致的接口设计 |
| 分层系统 | 支持代理、网关 |
HTTP 方法与 CRUD
| 方法 | 操作 | 幂等 | 安全 | 示例 |
|---|---|---|---|---|
| GET | 查询 | ✅ | ✅ | 获取用户列表 |
| POST | 创建 | ❌ | ❌ | 创建新用户 |
| PUT | 全量更新 | ✅ | ❌ | 更新整个用户 |
| PATCH | 部分更新 | ❌ | ❌ | 更新用户名 |
| DELETE | 删除 | ✅ | ❌ | 删除用户 |
幂等性
幂等性指多次执行相同操作,结果不变。GET、PUT、DELETE 是幂等的,POST 不是。
URL 设计规范
资源命名
// ✅ 推荐:使用名词复数
GET /users // 获取用户列表
GET /users/123 // 获取单个用户
POST /users // 创建用户
PUT /users/123 // 更新用户
DELETE /users/123 // 删除用户
// ❌ 不推荐:使用动词
GET /getUsers
GET /getUserById
POST /createUser
POST /deleteUser
资源层级
// 嵌套资源
GET /users/123/posts // 用户的文章列表
GET /users/123/posts/456 // 用户的某篇文章
POST /users/123/posts // 创建用户文章
// 但嵌套不宜过深(最多 2 层)
// ❌ 不推荐
GET /users/123/posts/456/comments/789
// ✅ 推荐:使用查询参数
GET /comments/789
GET /comments?postId=456&userId=123
查询参数
// 分页
GET /users?page=1&pageSize=20
GET /users?offset=0&limit=20
// 排序
GET /users?sort=created_at&order=desc
GET /users?sortBy=-createdAt // 减号表示降序
// 过滤
GET /users?status=active&role=admin
// 字段选择
GET /users?fields=id,name,email
// 搜索
GET /users?q=john
GET /users?search=john
状态码规范
成功响应
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | GET、PUT、PATCH 成功 |
| 201 | Created | POST 创建成功 |
| 204 | No Content | DELETE 成功 |
客户端错误
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未登录 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突 |
| 422 | Unprocessable | 参数校验失败 |
| 429 | Too Many Requests | 限流 |
服务端错误
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 500 | Internal Error | 服务端异常 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务不可用 |
响应格式设计
成功响应
// 单个资源
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// GET /users/123
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "John",
"email": "john@example.com"
}
}
// 列表资源(带分页)
interface PaginatedResponse<T> {
code: number;
message: string;
data: {
items: T[];
total: number;
page: number;
pageSize: number;
};
}
// GET /users?page=1&pageSize=20
{
"code": 0,
"message": "success",
"data": {
"items": [
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
],
"total": 100,
"page": 1,
"pageSize": 20
}
}
错误响应
interface ErrorResponse {
code: number;
message: string;
errors?: Array<{
field: string;
message: string;
}>;
}
// 400 Bad Request
{
"code": 40001,
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Password too short" }
]
}
版本控制
方式对比
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径 | /v1/users | 直观、易缓存 | URL 冗长 |
| 查询参数 | /users?version=1 | 灵活 | 易被忽略 |
| 请求头 | Accept: application/vnd.api.v1+json | 优雅 | 不直观 |
// 推荐:URL 路径版本
const API_V1 = '/api/v1';
const API_V2 = '/api/v2';
// GET /api/v1/users
// GET /api/v2/users
安全实践
认证方式
// 1. Bearer Token
fetch('/api/users', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
}
});
// 2. API Key
fetch('/api/users?api_key=xxx');
// 3. Cookie(适合同域)
fetch('/api/users', {
credentials: 'include'
});
请求签名
// 防止请求篡改
interface SignedRequest {
timestamp: number;
nonce: string;
signature: string; // HMAC-SHA256(params + secret)
}
限流策略
// 响应头中返回限流信息
// X-RateLimit-Limit: 100
// X-RateLimit-Remaining: 95
// X-RateLimit-Reset: 1609459200
TypeScript 封装
// 通用 API 请求封装
interface RequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
url: string;
params?: Record<string, any>;
data?: any;
headers?: Record<string, string>;
}
async function request<T>(config: RequestConfig): Promise<ApiResponse<T>> {
const { method, url, params, data, headers } = config;
// 构建 URL
let fullUrl = url;
if (params) {
const searchParams = new URLSearchParams(params);
fullUrl = `${url}?${searchParams}`;
}
const response = await fetch(fullUrl, {
method,
headers: {
'Content-Type': 'application/json',
...headers
},
body: data ? JSON.stringify(data) : undefined
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// 使用示例
interface User {
id: number;
name: string;
email: string;
}
// 获取用户列表
const users = await request<User[]>({
method: 'GET',
url: '/api/v1/users',
params: { page: 1, pageSize: 20 }
});
// 创建用户
const newUser = await request<User>({
method: 'POST',
url: '/api/v1/users',
data: { name: 'John', email: 'john@example.com' }
});
常见面试问题
Q1: RESTful API 的设计原则?
答案:
- 使用名词复数:
/users而非/user - HTTP 方法语义化:GET 查询、POST 创建、PUT 更新、DELETE 删除
- 正确的状态码:200 成功、201 创建、400 参数错误、401 未认证、404 不存在
- 无状态:每次请求包含所有必要信息
- 版本控制:
/api/v1/users - 统一响应格式:成功和错误都有一致的结构
Q2: PUT 和 PATCH 的区别?
答案:
| 区别 | PUT | PATCH |
|---|---|---|
| 更新方式 | 全量替换 | 部分更新 |
| 幂等性 | 幂等 | 非幂等 |
| 请求体 | 完整资源 | 仅修改字段 |
// PUT 全量更新
PUT /users/123
{
"name": "John",
"email": "john@example.com",
"age": 30,
"address": "..."
}
// PATCH 部分更新
PATCH /users/123
{
"name": "John Doe" // 只更新 name
}
Q3: 什么是幂等性?哪些方法是幂等的?
答案:
幂等性指同一操作执行多次,结果与执行一次相同。
| 方法 | 幂等 | 原因 |
|---|---|---|
| GET | ✅ | 只读操作 |
| PUT | ✅ | 全量替换,结果相同 |
| DELETE | ✅ | 删除后再删除,资源仍不存在 |
| POST | ❌ | 每次创建新资源 |
| PATCH | ❌ | 可能依赖当前状态 |
Q4: 如何处理批量操作?
答案:
// 方案1:批量端点
POST /users/batch
{
"users": [
{ "name": "John" },
{ "name": "Jane" }
]
}
// 方案2:批量删除
DELETE /users?ids=1,2,3
// 方案3:批量更新
PATCH /users
{
"ids": [1, 2, 3],
"data": { "status": "active" }
}
Q5: API 如何实现分页?
答案:
// 方案1:页码分页
GET /users?page=2&pageSize=20
// 响应
{
"data": [...],
"meta": {
"total": 100,
"page": 2,
"pageSize": 20,
"totalPages": 5
}
}
// 方案2:游标分页(大数据量推荐)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
// 响应
{
"data": [...],
"meta": {
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
}
游标分页优势:
- 避免深分页性能问题
- 数据一致性更好
- 适合无限滚动场景