RESTFul API接口设计指南_V2
API接口设计指南_V0.1.0
前言
接口是系统与外界交互的窗口,其他系统通过接口可以知道你管理着哪些资源,他能对这些资源干些什么。
当然我们不遵守规范或建议也可以满足上面的目标,既然如此我们为什么还要按RESTful的规范来设计我们的接口呢?
这样的灵魂拷问很现实也很真实,有小朋友会讲:
:::
我写接口都是一把梭,快得很,
什么RESTful?什么规范?
不存在的!
规范只会减慢我打字的速度。
:::
1. 核心理念:API即产品 (API as a Product)
在开始设计任何端点之前,我们必须树立一个核心理念:API是提供给其他开发者(无论是前端、移动端还是其他微服务)使用的产品。这意味着API的设计必须优先考虑:
-
开发者体验 (Developer Experience, DX): API是否易于理解、学习和使用?错误信息是否清晰明确?
-
健壮性 (Robustness): API在面对错误输入、依赖服务故障时,行为是否可预测?
-
可演化性 (Evolvability): API能否在不破坏现有客户端的情况下,平滑地进行功能迭代和升级?
-
安全性 (Security): API是否充分保护了数据和系统资源?
这篇文档中的所有规范,都源于以上核心理念。
2. RESTful 基础与命名规范
我们采用RESTful作为API设计的主要风格,它利用HTTP协议的语义来表达操作。
2.1. 资源路径 (URI)
URI代表“资源”,应全部使用名词复数,并采用**小写字母和连字符(kebab-case)**的组合。
-
【正例】
-
获取所有用户:
GET /users
-
获取ID为
123
的用户:GET /users/123
-
获取用户
123
的所有订单:GET /users/123/orders
-
获取用户
123
的最新订单:GET /users/123/latest-order
(latest-order作为单一资源)
-
-
【反例】
-
GET /getAllUsers
(动词出现在路径中) -
POST /user/create
(动词出现在路径中) -
GET /users_and_orders
(资源边界不清晰) -
GET /users/123/getOrder
(动词出现在路径中)
-
-
讲解:
-
优势: 遵循HTTP语义,路径清晰地描述了资源层次,而非操作。
kebab-case
增强了URL的可读性,并避免了大小写敏感性问题。 -
劣势: 对于非CRUD的复杂操作(如批量审核),可能需要引入
/actions
子资源或使用不同的设计模式,如RPC风格的端点。
-
2.2. HTTP 方法 (Verbs)
使用HTTP方法来描述对资源的操作。
-
GET
: 安全且幂等。用于读取资源,不应产生副作用。 -
POST
: 非幂等。用于创建子资源。 -
PUT
: 幂等。用于完整替换一个已存在的资源。 -
PATCH
: 幂等。用于部分更新一个已存在的资源。 -
DELETE
: 幂等。用于删除一个资源。 -
幂等性解释: 多次执行相同的操作,其结果与执行一次完全相同。这对于构建可靠的、可重试的客户端至关重要。
-
评审要点:
-
GET
请求是否被用于修改数据?(严重错误) -
更新操作是使用
PUT
还是PATCH
?如果客户端只发送了部分字段,使用PUT
可能会意外地将其他字段置为null
。
-
3. 数据传输结构 (DTO - Data Transfer Object)
一致、可预测的数据结构是提升开发者体验的关键。
3.1. 标准响应体封装
所有API响应都应包裹在一个标准结构中,方便客户端进行统一处理。
- 【正例】
// 标准响应结构
export interface ApiResponse<T> {success: boolean; // 操作是否成功code: number; // 业务状态码 (非HTTP状态码)message: string | null; // 提示信息data: T | null; // 成功时的数据error?: ApiError; // 失败时的详细错误信息
}// 详细错误对象
export interface ApiError {type: string; // 错误类型 (e.g., VALIDATION_ERROR, AUTHENTICATION_ERROR)details?: Record<string, string>; // 详细信息,如字段校验失败
}
-
【反例】
-
成功时直接返回数据:
[{ "id": 1, "name": "..." }]
-
失败时返回不同的结构:
{ "error": "Invalid ID", "reason": "..." }
-
-
讲解:
- 优势:
-
客户端统一处理: Vue客户端可以创建一个统一的拦截器来处理所有
success: false
的情况,无需在每个业务组件中重复编写错误处理逻辑。-
元数据承载: 可以在响应中携带分页、追踪ID等元数据。
-
业务码分离: HTTP状态码表达协议层面的状态(如401未授权),而业务码
code
可以表达更精细的业务逻辑状态(如20001-库存不足)。
-
-
劣势: 增加了少量的网络负载。
3.2. 分页 (Pagination)
对于返回集合资源的接口,必须实现分页,以防止数据量过大拖垮服务器和客户端。
- 【正例】 在
data
字段中包含分页信息。
// 在 ApiResponse.data 中
{"page": 1, // 当前页码"size": 20, // 每页数量"totalElements": 153, // 总条目数"totalPages": 8, // 总页数"content": [/* 列表数据 */]
}
请求参数: GET /users?page=1&size=20
-
【反例】
-
GET /users
返回所有用户数据。 -
分页信息放在HTTP Headers中,对客户端不友好。
-
-
讲解: 将分页信息和数据内容一起返回,是最直观、对前端最友好的方式。
3.3. 日期和时间
所有日期和时间字段都应使用 UTC 时间,并遵循 ISO 8601 格式。
-
【正例】:
"2025-09-08T15:30:00.123Z"
-
【反例】:
"2025/09/08 15:30"
,1725780600
(Unix时间戳,可读性差) -
讲解: ISO 8601是全球标准,几乎所有语言都有内置的库来解析它,避免了时区转换的混乱。
3.4. 空值处理
-
不存在的字段: 不返回该字段。
-
值为
**null**
的字段: 明确返回null
。 -
空集合: 返回空数组
[]
,而不是null
。
4. API版本管理 (Versioning)
-
推荐方式: 在URL中加入版本号,如
/v1/users
。 -
讲解:
-
优势: 最直观,对开发者和浏览器都非常友好。可以方便地对不同版本的API进行路由和负载均衡。
-
劣势: 路径中包含了版本信息,不够“纯粹”。(但工程上,清晰度远比纯粹性重要)
-
时机: 只有在发生**破坏性变更(Breaking Change)**时(如删除字段、修改字段类型)才需要升级版本号。新增字段属于非破坏性变更。
-
5. 高级设计模式
5.1. 过滤、排序和字段选择
允许客户端按需索取数据,是提升性能和灵活性的关键。
-
过滤:
GET /orders?status=shipped&customerId=123
-
排序:
GET /users?sort=createdAt,desc
(按创建时间降序) -
字段选择 (稀疏字段集):
GET /users/123?fields=id,name,email
-
讲解:
-
优势: 极大地减少了网络负载,对于移动端或低带宽环境尤其重要。后端也可以根据
fields
参数优化数据库查询,避免查询不必要的列。 -
实现: 后端可以使用规范解析库(如 RSQL)来安全地将查询参数转换为数据库查询。
-
5.2. 实时通信与服务器推送事件 (SSE - Server-Sent Events)
对于需要服务器向客户端单向推送实时数据的场景(如站内信、状态更新、日志流),SSE是比WebSocket更轻量、更简单的选择。
-
场景: 后端任务执行进度通知。
-
讲解:
- 优势:
-
基于HTTP: 无需新的协议或端口,易于与现有基础设施(如Nginx)集成。
-
简单: 客户端API非常简单 (
EventSource
API),且支持自动重连。 -
轻量: 相比WebSocket,协议开销更小。
-
-
劣势: 只能实现服务器到客户端的单向通信。
6. 安全 (Security)
-
认证 (Authentication): 推荐使用基于 OAuth2/OIDC 的 Token 认证(如 JWT)。Token通过
Authorization: Bearer <token>
HTTP头传递。 -
授权 (Authorization): 在微服务网关或服务内部,根据用户的角色和权限(Scopes)对API访问进行控制。
-
HTTPS: 所有API通信必须使用HTTPS加密。
7. 实现样例 (Vue + TypeScript + Spring Cloud)
场景:获取用户列表并实时接收用户创建通知
7.1. 后端实现 (Spring Cloud)
1. 标准响应与错误处理
// ApiResponse.java (使用泛型)
@Data
public class ApiResponse<T> {private boolean success;private int code;private String message;private T data;// 省略构造函数...
}// GlobalExceptionHandler.java (统一异常处理)
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public ApiResponse<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {// ...构造详细的ApiError对象return new ApiResponse<>(false, 40001, "Validation Failed", null);}
}
2. 用户Controller (REST API)
@RestController
@RequestMapping("/v1/users")
public class UserController {@Autowiredprivate UserService userService;// 【正例】使用DTO,实现了分页、排序@GetMappingpublic ApiResponse<Page<UserDTO>> getUsers(@RequestParam(defaultValue = "0") int page,@RequestParam(defaultValue = "20") int size,@RequestParam(required = false) String sort) {Pageable pageable = PageRequest.of(page, size, Sort.by(sort)); // 简化版排序Page<UserDTO> userPage = userService.findAll(pageable);return new ApiResponse<>(true, 200, "Success", userPage);}
}
3. SSE Controller
@RestController
@RequestMapping("/v1/events")
public class EventController {private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();// 【正例】客户端订阅SSE事件@GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter subscribe(String clientId) {SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);emitters.put(clientId, emitter);emitter.onCompletion(() -> emitters.remove(clientId));emitter.onTimeout(() -> emitters.remove(clientId));return emitter;}// 当有新用户创建时,调用此方法推送事件public void sendUserCreatedEvent(UserDTO newUser) {emitters.forEach((clientId, emitter) -> {try {emitter.send(SseEmitter.event().name("user-created") // 事件类型.data(newUser)); // 事件数据} catch (IOException e) {emitter.completeWithError(e);}});}
}
7.2. 前端实现 (Vue 3 + TypeScript)
1. API客户端 (封装axios)
// api/client.ts
import axios from 'axios';
import type { ApiResponse } from './types'; // 引入类型定义const apiClient = axios.create({ baseURL: '/api' });apiClient.interceptors.response.use((response) => {// 【正例】统一处理ApiResponse结构const apiResponse: ApiResponse<any> = response.data;if (apiResponse.success) {return apiResponse.data; // 直接返回data字段给业务逻辑} else {// 统一处理业务错误,如弹窗提示showToast(apiResponse.message || '操作失败');return Promise.reject(apiResponse);}},(error) => {// 处理HTTP层面的错误showToast('网络请求失败');return Promise.reject(error);}
);export default apiClient;
2. Vue组件中使用
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import apiClient from '@/api/client';
import type { UserDTO, Page } from '@/api/types';const users = ref<UserDTO[]>([]);
const eventSource = ref<EventSource | null>(null);// 【正例】调用分页接口
const fetchUsers = async () => {try {const userPage: Page<UserDTO> = await apiClient.get('/v1/users?page=0&size=10');users.value = userPage.content;} catch (error) {console.error("Failed to fetch users:", error);}
};// 【正例】监听SSE事件
const setupSseListener = () => {const clientId = 'unique-client-id'; // 应由认证系统生成eventSource.value = new EventSource(`/api/v1/events/subscribe?clientId=${clientId}`);eventSource.value.addEventListener('user-created', (event) => {const newUser = JSON.parse(event.data) as UserDTO;users.value.unshift(newUser); // 在列表顶部添加新用户showNotification(`新用户创建: ${newUser.name}`);});eventSource.value.onerror = () => {console.error("SSE connection error.");// EventSource会在此处自动尝试重连};
};onMounted(() => {fetchUsers();setupSseListener();
});onUnmounted(() => {// 组件销毁时关闭连接eventSource.value?.close();
});
</script>
【反例】前端代码
// 在组件中直接使用axios,没有统一封装和错误处理
axios.get('/api/v1/users').then(response => {// 每次都要判断response.data.successif(response.data.success) {users.value = response.data.data.content;} else {alert(response.data.message);}
}).catch(err => {alert('网络错误');
});
- 劣势: 模板代码重复,错误处理分散,难以维护。
8. 云原生友好性
为了让服务在Kubernetes等云原生环境中更好地被管理(MCP - Managed Control Plane),API应提供:
-
健康检查端点:
-
GET /actuator/health/liveness
: 服务是否存活。 -
GET /actuator/health/readiness
: 服务是否准备好接收流量。
-
-
结构化日志: 所有API请求和响应的关键信息应以JSON格式输出到日志,方便日志系统(如ELK, Loki)收集和分析。