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非常简单 (
EventSourceAPI),且支持自动重连。 -
轻量: 相比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 {
@Autowired
private UserService userService;
// 【正例】使用DTO,实现了分页、排序
@GetMapping
public 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.success
if(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)收集和分析。
1607

被折叠的 条评论
为什么被折叠?



