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": "..." }

  • 讲解:

    • 优势:
  1. 客户端统一处理: Vue客户端可以创建一个统一的拦截器来处理所有success: false的情况,无需在每个业务组件中重复编写错误处理逻辑。

    1. 元数据承载: 可以在响应中携带分页、追踪ID等元数据。

    2. 业务码分离: HTTP状态码表达协议层面的状态(如401未授权),而业务码code可以表达更精细的业务逻辑状态(如20001-库存不足)。

  2. 劣势: 增加了少量的网络负载。

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更轻量、更简单的选择。

  • 场景: 后端任务执行进度通知。

  • 讲解:

    • 优势:
  1. 基于HTTP: 无需新的协议或端口,易于与现有基础设施(如Nginx)集成。

    1. 简单: 客户端API非常简单 (EventSource API),且支持自动重连。

    2. 轻量: 相比WebSocket,协议开销更小。

  2. 劣势: 只能实现服务器到客户端的单向通信。

6. 安全 (Security)

  • 认证 (Authentication): 推荐使用基于 OAuth2/OIDCToken 认证(如 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)收集和分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值