跳到主要内容

RESTful API 设计

问题

什么是 RESTful API?如何设计符合规范的 API 接口?

面试速答版

什么是 RESTful API? 一句话:用 HTTP 方法操作资源 (URL) 的 API 风格:

  • 资源用名词,URL 表示「操作什么」(/users/123),HTTP 方法表示「怎么操作」(GET/POST/PUT/DELETE)。
  • 六大约束里前端最常感知的是:无状态(每个请求自带凭证,服务端不存上下文)和统一接口
  • 不是非要严格 REST,业界更多是「类 REST」,配合状态码 + JSON 响应就够用。

设计规范要点有哪些? 几个易踩坑的点:

  • URL 用名词复数 + 不带动词/users/123/posts 而不是 /getUserPosts?id=123
  • 嵌套不超过 2 层,更深的层级靠 query 参数表达:/comments?postId=456
  • HTTP 方法语义正确PUT 是全量替换且幂等,PATCH 是部分更新不幂等。
  • 状态码要用对200 成功、201 新建成功、204 删除成功、400 参数错、401 未登录、403 无权限、404 资源不存在、409 冲突、422 校验失败、429 限流。
  • 版本走 URL (/api/v1/users) 最直观,比 Header 方式好排查。

RESTful 的局限是什么?

  • 多次往返:拿一个页面要的数据可能要打 3-5 个接口。
  • 过度获取/获取不足:服务端返回字段固定,前端要么浪费带宽要么再发请求。
  • 复杂关系不好表达:图状数据用 REST 拼 URL 很别扭。
  • 所以才有了 GraphQL 和 BFF。

答案

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
}
}

游标分页优势:

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

相关链接