Skip to content
第 219 / 250 章架构⏱ 12 分钟阅读

第 219 章:API 设计规范

学习目标

  • 掌握 RESTful API 设计规范
  • 统一响应格式
  • 学会错误码体系
  • 理解 GraphQL 与 gRPC 选型

一、RESTful 设计规范

1.1 资源建模

把业务抽象为资源(名词),操作对应 HTTP 方法。

❌ /createOrder
❌ /getUser
✅ /orders        POST 创建
✅ /users/{id}    GET 查询

1.2 HTTP 方法语义

方法幂等语义
GET获取资源
POST新建资源
PUT全量替换
PATCH部分更新
DELETE删除资源

1.3 URI 设计

✅ 推荐:
GET    /users                          # 列表
GET    /users/123                      # 详情
POST   /users                          # 创建
PUT    /users/123                      # 替换
PATCH  /users/123                      # 部分更新
DELETE /users/123                      # 删除

# 子资源
GET    /users/123/orders               # 用户的订单
POST   /users/123/orders               # 给用户下单

# 过滤 / 排序 / 分页
GET    /users?status=active&sort=-created_at&page=1&size=20

# 操作(动词)
POST   /users/123/avatar               # 上传头像
POST   /orders/123/cancel              # 取消订单

❌ 不要在 URI 中用动词:
GET /getUsers
POST /createOrder

1.4 复数 vs 单数

✅ 推荐复数: /users  /orders  /products
理由: URI 表"资源集合",概念更清晰

1.5 路径 vs Header

用途推荐位置
业务参数路径 / 查询参数
鉴权Header (Authorization)
幂等Header (Idempotency-Key)
版本URL 或 Header
业务上下文Header (X-User-Id, X-Tenant-Id)

1.6 HATEOAS

返回资源时携带相关链接,降低耦合。

json
{
  "id": 123,
  "name": "Tom",
  "status": "active",
  "_links": {
    "self": { "href": "/users/123" },
    "orders": { "href": "/users/123/orders" },
    "avatar": { "href": "/users/123/avatar" }
  }
}

二、统一响应格式

2.1 通用 Result

java
@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;
    private String traceId;

    public static <T> Result<T> ok() {
        return build(200, "success", null);
    }

    public static <T> Result<T> ok(T data) {
        return build(200, "success", data);
    }

    public static <T> Result<T> error(int code, String message) {
        return build(code, message, null);
    }

    public boolean isSuccess() {
        return code == 200;
    }
}

使用:

java
@GetMapping("/users/{id}")
public Result<UserDTO> get(@PathVariable Long id) {
    return Result.ok(userService.getById(id));
}

2.2 分页响应

java
@Data
public class PageResult<T> {
    private List<T> items;
    private long total;
    private int page;
    private int size;

    public static <T> PageResult<T> of(List<T> items, long total, int page, int size) {
        PageResult<T> r = new PageResult<>();
        r.items = items;
        r.total = total;
        r.page = page;
        r.size = size;
        return r;
    }
}

2.3 响应示例

成功:

json
{
  "code": 200,
  "message": "success",
  "data": {
    "id": 123,
    "name": "Tom"
  },
  "traceId": "a1b2c3"
}

失败:

json
{
  "code": 40001,
  "message": "参数错误:用户ID不能为空",
  "data": null,
  "traceId": "a1b2c3"
}

三、错误码体系

3.1 设计原则

  • 全数字:便于跨语言传递
  • 分层结构:业务码 + 子码
  • 易于定位:从错误码可看出模块

3.2 结构

python
5位数字: AABCC
AA = 服务编码(2位)
B  = 错误级别(1位)
CC = 具体错误(2位)

例:
10001 = 用户服务-参数错误
10101 = 用户服务-未找到
10201 = 用户服务-已存在
20001 = 订单服务-参数错误
20002 = 订单服务-余额不足
20003 = 订单服务-库存不足

3.3 错误级别

范围含义
0xx系统级错误
1xx用户服务
2xx订单服务
4xx参数校验失败
5xx权限 / 鉴权

3.4 HTTP 状态码 vs 业务码

java
@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public Result<User> get(@PathVariable Long id) {
        User user = userService.findById(id);
        if (user == null) {
            // HTTP 状态 404 + 业务码 10101
            return ResponseEntity.status(404).body(
                Result.error(10101, "用户不存在")
            );
        }
        return ResponseEntity.ok(Result.ok(user));
    }
}

原则:业务码用于定位,HTTP 状态用于协议识别。

场景HTTP业务码
成功200/201200
客户端错误4004xxxx
未授权401401xx
禁止访问403403xx
未找到404404xx
服务器错误5005xxxx

四、字段命名规范

4.1 命名风格

java
// JSON 用 snake_case 还是 camelCase?

// 主流: camelCase (Java/JS)
{
  "userId": 1,
  "userName": "Tom"
}

// 一些场景用 snake_case (Python/Ruby)
{
  "user_id": 1,
  "user_name": "Tom"
}

建议:Java 栈用 camelCase,跨语言用 snake_case。

4.2 时间字段

json
// 字符串(推荐,跨时区安全)
"createdAt": "2026-08-13T10:30:00+08:00"
"createdAt": "2026-08-13 10:30:00"

// 时间戳(便于计算)
"createTime": 1691896200000

4.3 金额

json
// 分(整数,避免浮点精度)
"amount": 1999        // ¥19.99

// Decimal 字符串
"amount": "19.99"

4.4 空值与默认值

java
@Data
public class UserDTO {
    private String name;
    private Integer age;        // null 表示未知,不是 0
    private BigDecimal balance;
    @JsonInclude(JsonInclude.Include.NON_NULL)  // null 不序列化
    private String phone;
}

五、版本管理

5.1 URL 版本(常用)

/api/v1/users
/api/v2/users

适用:版本差异大、独立维护

5.2 Header 版本

GET /api/users
Accept: application/vnd.myapp.v2+json

适用:同接口多版本

5.3 查询参数版本

GET /api/users?version=2

适用:不推荐用于生产

5.4 兼容性原则

  • ✅ 添加新字段(消费者忽略未知字段)
  • ✅ 添加新端点
  • ❌ 修改字段类型
  • ❌ 重命名字段
  • ❌ 删除字段(标记 deprecated,过两个版本再删)

六、GraphQL

6.1 解决什么问题

传统 REST:前端要拼接多次请求

GraphQL:一次请求获取所有数据

graphql
query {
  user(id: 123) {
    name
    avatar
    orders(first: 10) {
      id
      amount
      status
    }
  }
}

6.2 Spring Boot + GraphQL

java
@Controller
public class UserController {

    @QueryMapping
    public UserDTO userById(@Argument Long id) {
        return userService.findById(id);
    }

    @SchemaMapping(typeName = "User", field = "orders")
    public List<OrderDTO> orders(UserDTO user) {
        return orderService.findByUser(user.getId());
    }
}
graphql
# schema.graphqls
type User {
  id: ID!
  name: String
  avatar: String
  orders(first: Int = 10): [Order!]!
}

6.3 何时用 GraphQL

适合不适合
多端(移动/Web)字段差异大简单 CRUD
数据图(实体关系复杂)性能极敏感场景(N+1)
前端迭代快小团队 / 简单业务

七、gRPC

7.1 特点

  • HTTP/2 多路复用
  • Protobuf 二进制(快 5-10 倍)
  • 强类型契约
  • 支持 4 种通信方式

7.2 通信模式

protobuf
// 1. Unary
rpc GetUser(UserRequest) returns (UserResponse);

// 2. Server Streaming
rpc ListUsers(ListRequest) returns (stream User);

// 3. Client Streaming
rpc Upload(stream Chunk) returns (UploadResult);

// 4. Bidirectional
rpc Chat(stream Message) returns (stream Message);

7.3 何时用 gRPC

适合不适合
服务间高频低延迟调用外部 API(浏览器难调)
强契约(团队间)需要流式上传/可读文本
多语言服务调试时只看 log

八、API 文档

8.1 OpenAPI 3.0(Swagger)

yaml
openapi: 3.0.0
info:
  title: 用户服务 API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: 查询用户
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

8.2 springdoc-openapi(推荐)

java
@Operation(summary = "查询用户")
@GetMapping("/users/{id}")
public UserDTO get(
    @Parameter(description = "用户ID") @PathVariable Long id
) {
    return userService.get(id);
}

访问: http://localhost:8080/swagger-ui.html

九、安全规范

9.1 鉴权

yaml
# API Gateway 统一鉴权
spring:
  cloud:
    gateway:
      routes:
        - id: protected-route
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: AuthFilter    # 校验 JWT

9.2 HTTPS

所有外部 API 必须 HTTPS
内部服务可 mTLS

9.3 限流

java
// 网关侧限流
@SentinelResource("createOrder")
@PostMapping("/orders")
public Result<Order> create(...) { ... }

9.4 防重放

  • 时间戳 + 签名
  • 一次性 token
  • 幂等键

十、API 网关聚合

聚合多个服务调用,减少客户端请求,降低复杂度。

十一、本章小结

主题要点
RESTful资源命名 + HTTP 方法语义
响应统一封装 + 分页
错误码分层设计 + HTTP 状态
版本URL 路径 + 兼容原则
GraphQL多端聚合场景
gRPC内部高频调用
文档OpenAPI / springdoc
安全鉴权 + HTTPS + 限流

动手练习

  1. 为订单服务设计完整 RESTful API(列表、详情、创建、状态流转、取消)
  2. 实现通用 Result 封装,含分页能力
  3. 定义错误码体系(订单 + 商品 + 库存三服务)
  4. 用 springdoc-openapi 生成接口文档

推荐阅读


下一章:第 220 章:事件驱动架构

本站基于 VitePress 构建 · 由 Codebook 团队维护