人人都会AI编程

12.7 RESTful API 设计规范与最佳实践

更新时间:2026-07-11

REST(Representational State Transfer)并非一种协议,而是一组基于 HTTP 协议的架构约束与设计原则。遵循这些原则构建的 API,能够天然具备无状态、可缓存、分层系统等优势。然而,真正生产环境中可靠、易用、易维护的 RESTful API,还需要在规范细节和工程实践上下一番功夫。本节将围绕 URL 设计、HTTP 方法语义、状态码、版本管理、错误处理、分页过滤、安全等维度,给出可直接落地的设计规范与 Spring 生态中的最佳实践。

12.7.1 资源导向的 URL 设计

RESTful API 的核心是资源,每个 URL 应该代表一个资源或资源集合,而非动作。

1. 使用名词,避免动词

# 推荐
GET    /users          # 获取用户列表
GET    /users/{id}     # 获取指定用户
POST   /users          # 创建用户
PUT    /users/{id}     # 完整更新用户
PATCH  /users/{id}     # 部分更新用户
DELETE /users/{id}     # 删除用户

# 不推荐
GET  /getUserList
POST /createUser
POST /deleteUser

2. 资源嵌套层次不宜过深

表示关联关系时,采用嵌套资源,但深度一般不超过两层。

# 推荐
GET /users/{userId}/orders          # 获取某用户所有订单
GET /users/{userId}/orders/{orderId} # 获取某用户指定订单

# 可替代方案:若嵌套过深,可使用顶级资源加查询参数
GET /orders?userId=123&status=paid

3. 集合用复数,文档用单数标识

统一使用复数名词表示集合,用路径参数中的 ID 标识具体资源。

GET  /products          # 产品集合
GET  /products/{id}     # 单个产品

4. URL 中不使用文件扩展名

API 的表述格式应由 AcceptContent-Type 头协商,而非 URL 中的 .json.xml

# 推荐
GET /users/123
Accept: application/json

# 不推荐
GET /users/123.json

12.7.2 正确使用 HTTP 方法

HTTP 方法的语义必须与操作匹配,这是 RESTful API 最具契约性的部分。

| 方法 | 语义 | 幂等性 | 安全性 | 用途 |
|------|------|--------|--------|------|
| GET | 获取资源表述 | 是 | 是 | 查询操作 |
| POST | 创建资源或触发操作 | 否 | 否 | 新增、复杂查询 |
| PUT | 完整替换资源 | 是 | 否 | 全量更新 |
| PATCH | 部分更新资源 | 否 | 否 | 增量更新 |
| DELETE | 删除资源 | 是 | 否 | 删除 |

使用示例:

POST   /users          # 创建用户,返回 201 Created 及 Location 头
PUT    /users/{id}     # 全量替换用户资源,需传递完整对象
PATCH  /users/{id}     # 仅更新部分字段,如 {"email":"new@example.com"}
DELETE /users/{id}     # 删除用户,返回 204 No Content

不以动词冒充资源: 若确实有非 CRUD 的“动作”,应将其作为资源的子资源或复合名词处理。

# 推荐:将“激活”视为用户的一个子资源
POST /users/{id}/activation

# 或者使用命令式资源
POST /password-resets

12.7.3 善用 HTTP 状态码

状态码是 RESTful API 表达结果的直接语言,而不应把业务错误全部塞进响应体中。

常用状态码分类:

  • 2xx 成功
  • 200 OK:请求成功(GET、PUT、PATCH 常用)
  • 201 Created:资源创建成功(配合 Location 头返回新资源 URI)
  • 204 No Content:成功但无响应体(DELETE 成功、更新成功但无需返回内容)
  • 3xx 重定向
  • 301 Moved Permanently:资源 URI 永久变更
  • 304 Not Modified:资源未改动,可使用缓存
  • 4xx 客户端错误
  • 400 Bad Request:请求参数错误、格式不对
  • 401 Unauthorized:未认证或认证失效
  • 403 Forbidden:认证通过但无权限
  • 404 Not Found:资源不存在或无权访问
  • 409 Conflict:资源冲突(如重复提交、版本冲突)
  • 422 Unprocessable Entity:语义错误(常用于参数校验失败)
  • 5xx 服务端错误
  • 500 Internal Server Error:未预期异常
  • 503 Service Unavailable:服务临时不可用

实践要求:

  • 不将业务错误码混入 HTTP 状态码,例如用 200 包裹错误信息(如 {"code":500,"message":"error"})。应让 HTTP 状态码直接表达结果。
  • 在响应体中可提供更详细的业务错误码和描述,便于客户端精细化处理。
// 400 Bad Request 时的响应体示例
{
  "error": "validation_failed",
  "message": "请求参数校验不通过",
  "details": [
    { "field": "email", "message": "邮箱格式不正确" }
  ]
}

12.7.4 API 版本控制

随着业务演进,API 难免需要发布不兼容的变更。选择合适的版本控制策略可以避免客户端被意外破坏。

常见方案:

  1. URI 路径版本(最常用)
   GET /v1/users
   GET /v2/users
   

清晰直观,便于路由和文档化管理。

  1. 请求头版本
   GET /users
   Accept: application/vnd.myapp.v2+json
   

URI 保持干净,但对客户端调试略不友好。

  1. 查询参数版本
   GET /users?version=2
   

简便但不推荐作为长期策略。

推荐实践:

  • 新项目优先采用 URI 路径版本,简单且易于网关层控制。
  • 同一大版本内应保持向后兼容,新字段采用“只增不减”原则。
  • 废弃的 API 需要提供 Sunset 头 或在响应中添加 Deprecation 头,明确告知客户端迁移时间线。
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT

12.7.5 标准化错误响应

一个友好且统一的错误格式能显著降低客户端集成的成本。

推荐结构:

{
  "error": "invalid_token",
  "error_description": "访问令牌已过期",
  "error_uri": "https://api.example.com/docs/errors#invalid_token",
  "timestamp": "2025-01-15T10:23:45Z",
  "trace_id": "abc123"
}

Spring 中的实践:

通过 @ControllerAdvice 结合 ResponseEntityExceptionHandler 统一处理异常,将各类业务异常映射为统一的响应结构。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
        List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
        ErrorResponse error = new ErrorResponse(
            "validation_failed",
            "请求参数校验失败",
            fieldErrors.stream()
                .map(e -> new FieldErrorDetail(e.getField(), e.getDefaultMessage()))
                .collect(Collectors.toList())
        );
        return ResponseEntity.badRequest().body(error);
    }
}

12.7.6 分页、过滤、排序与字段选择

当资源集合的数据量较大时,API 必须提供灵活的数据检索能力。

1. 分页

采用基于偏移量或游标的方式。

  • 偏移量分页:
  GET /users?page=0&size=20
  

响应中返回分页元数据:

  {
    "data": [...],
    "page": {
      "size": 20,
      "number": 0,
      "totalElements": 200,
      "totalPages": 10
    }
  }
  
  • 游标分页(推荐用于实时数据流):
  GET /users?cursor=eyJsYXN0SWQiOjEyM30&limit=20
  

避免因数据插入导致的重复或遗漏。

2. 过滤

GET /users?status=active&role=admin

复杂过滤条件可使用标准化的查询语法,如 filter 参数,但需注意避免过度设计。

3. 排序

GET /users?sort=createdAt,desc&sort=lastName,asc

4. 字段选择

允许客户端指定返回的字段,减少带宽消耗。

GET /users?fields=id,email,profile.name

Spring 中可使用 Jackson 或自定义注解配合响应过滤实现。

12.7.7 HATEOAS:让 API 自描述

成熟的 RESTful API 应向客户端提供可操作的状态转移链接,即 HATEOAS(Hypermedia as the Engine of Application State)。这样客户端无需硬编码 URI,而是根据响应中的链接驱动后续操作。

Spring HATEOAS 实践:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @GetMapping("/{id}")
    public EntityModel<Order> getOrder(@PathVariable Long id) {
        Order order = orderService.findById(id);
        return EntityModel.of(order,
            linkTo(methodOn(OrderController.class).getOrder(id)).withSelfRel(),
            linkTo(methodOn(OrderController.class).cancel(id)).withRel("cancel"),
            linkTo(methodOn(PaymentController.class).pay(id)).withRel("payment")
        );
    }
}

响应示例:

{
  "id": 1001,
  "status": "pending",
  "_links": {
    "self": { "href": "/orders/1001" },
    "cancel": { "href": "/orders/1001/cancel" },
    "payment": { "href": "/orders/1001/payment" }
  }
}

即使不完全实现 HATEOAS,至少应在响应中包含 self 链接,方便日志追踪和调试。

12.7.8 安全与幂等性

1. 强制 HTTPS

生产环境禁止使用明文 HTTP,所有 API 调用应通过 HTTPS 加密。

2. 身份认证与授权

  • 公开 API 使用 OAuth2 + JWT 或 API Key。
  • 内部服务间调用使用 mTLS 或服务网格。
  • 凭证绝不放在 URL 中,应使用 Authorization 头:Authorization: Bearer <token>

3. 幂等性保障

对于非幂等的 POST 请求,需提供幂等键(Idempotency-Key)机制,防止网络重试导致重复创建资源。

POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

服务端以该键作为唯一标识缓存请求结果,重复请求直接返回已处理的结果。

4. 跨域(CORS)配置

若 API 面临浏览器前端调用,需配置 CORS 策略,明确允许的来源、方法和头信息。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com")
                .allowedMethods("GET","POST","PUT","DELETE")
                .allowCredentials(true);
    }
}

12.7.9 文档化与开发体验

没有文档的 API 等于没有 API。利用 Spring REST Docs 或 OpenAPI(原 Swagger)自动生成准确、可读性强的文档。

  • Spring REST Docs:基于测试生成,确保文档与代码实际行为一致。
  • Springdoc-openapi:基于注解自动生成 OpenAPI 规范,可在运行时通过 Swagger UI 浏览。

最佳实践:

  • 每个 API 必须提供请求/响应示例。
  • 标注认证要求和可能的错误码。
  • 提供变更日志和废弃时间表。
  • 使用代码生成工具(如 OpenAPI Generator)从规范生成客户端 SDK,保持多端一致。

12.7.10 性能与缓存

充分利用 HTTP 缓存机制来减少服务端压力和响应延迟。

  • ETag / If-None-Match:用于资源变更检查,返回 304 Not Modified
  • Last-Modified / If-Modified-Since:基于时间戳的缓存验证。
  • Cache-Control:对于不常变化的资源(如字典数据),设置 max-agepublic

在 Spring 中可使用 WebContentInterceptor 或手动设置响应头实现缓存策略。

此外,合理使用 Gzip 压缩、分页限制、速率限制(Rate Limiting)等手段,保证 API 的稳定性和吞吐能力。

小结

设计优雅的 RESTful API 并非单纯套用规则,而是在一致性、可维护性和用户体验之间持续权衡。以资源为中心,利用 HTTP 完整语义,保持版本与错误处理的规范,并不断迭代文档,你的 API 就能从“能用”走向“好用”,成为团队内外部真正值得信赖的基础设施。