跳到主要内容

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

状态码规范

成功响应

状态码含义使用场景
200OKGET、PUT、PATCH 成功
201CreatedPOST 创建成功
204No ContentDELETE 成功

客户端错误

状态码含义使用场景
400Bad Request请求参数错误
401Unauthorized未登录
403Forbidden无权限
404Not Found资源不存在
409Conflict资源冲突
422Unprocessable参数校验失败
429Too Many Requests限流

服务端错误

状态码含义使用场景
500Internal Error服务端异常
502Bad Gateway网关错误
503Service 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 的设计原则?

答案

  1. 使用名词复数/users 而非 /user
  2. HTTP 方法语义化:GET 查询、POST 创建、PUT 更新、DELETE 删除
  3. 正确的状态码:200 成功、201 创建、400 参数错误、401 未认证、404 不存在
  4. 无状态:每次请求包含所有必要信息
  5. 版本控制/api/v1/users
  6. 统一响应格式:成功和错误都有一致的结构

Q2: PUT 和 PATCH 的区别?

答案

区别PUTPATCH
更新方式全量替换部分更新
幂等性幂等非幂等
请求体完整资源仅修改字段
// 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
}
}

游标分页优势:

  • 避免深分页性能问题
  • 数据一致性更好
  • 适合无限滚动场景

相关链接