第 21 课:API 设计 — RESTful 模式与规范

13 阅读9分钟

所属阶段:第四阶段「语言与框架」(第 17-22 课) 前置条件:第 17 课(后端语言)、第 20 课(数据库模式) 本课收获:一份符合 ECC 规范的 API 设计方案


一、本课概述

API 是前后端的契约,是微服务之间的桥梁。一个设计糟糕的 API 会让前端开发者抓狂,让后端维护变成噩梦。ECC 通过 api-design Skill 和框架专用 Skill 提供了一套完整的 API 设计规范。

本课回答三个问题:

  1. api-design Skill 的核心规范是什么? — 从资源命名到错误响应
  2. 不同框架如何实现这些规范? — Django、Spring Boot、NestJS 等
  3. 后端四层架构如何组织 API 代码? — Controller → Service → Repository → Database

二、api-design Skill 核心

2.1 资源命名

第一条规则:使用复数名词

✓ /api/v1/users           — 用户集合
✓ /api/v1/users/123       — 单个用户
✓ /api/v1/users/123/orders — 用户的订单集合

✗ /api/v1/user             — 单数
✗ /api/v1/getUsers         — 动词
✗ /api/v1/user-list        — 描述性名词

第二条规则:用 HTTP 方法表达动作

操作HTTP 方法URL含义
查询列表GET/users获取用户列表
查询单个GET/users/123获取指定用户
创建POST/users创建新用户
全量更新PUT/users/123替换整个用户对象
部分更新PATCH/users/123更新部分字段
删除DELETE/users/123删除指定用户

第三条规则:嵌套资源不超过两层

✓ /users/123/orders              — 两层:用户 → 订单
✓ /orders/456/items              — 两层:订单 → 订单项

✗ /users/123/orders/456/items/789/reviews  — 四层,太深了
✓ /order-items/789/reviews       — 扁平化处理

2.2 HTTP 状态码选择

api-design Skill 定义了明确的状态码使用规则:

成功响应

状态码使用场景示例
200 OKGET 成功、PUT/PATCH 成功返回查询结果或更新后的资源
201 CreatedPOST 创建成功返回新创建的资源 + Location 头
204 No ContentDELETE 成功不返回 body

客户端错误

状态码使用场景示例
400 Bad Request请求格式错误、参数校验失败缺少必填字段、类型错误
401 Unauthorized未认证未提供 token 或 token 过期
403 Forbidden已认证但无权限普通用户访问管理员接口
404 Not Found资源不存在用户 ID 不存在
409 Conflict资源冲突邮箱已注册
422 Unprocessable Entity业务规则校验失败余额不足
429 Too Many Requests超出速率限制包含 Retry-After 头

服务端错误

状态码使用场景
500 Internal Server Error未预期的服务端错误
502 Bad Gateway上游服务不可用
503 Service Unavailable服务维护中

关键原则401403 的区别 — 401 表示"你是谁?"(认证),403 表示"我知道你是谁,但你没权限"(授权)。

2.3 分页与过滤

分页参数

GET /api/v1/users?page=2&limit=20

参数说明:
  page  — 页码(从 1 开始)
  limit — 每页数量(默认 20,最大 100)

过滤参数

GET /api/v1/users?status=active&role=admin&created_after=2024-01-01

规则:
  ✓ 使用 snake_case 参数名
  ✓ 时间格式用 ISO 8601
  ✓ 布尔值用 true/false
  ✗ 不要在 URL 中放 JSON

排序参数

GET /api/v1/users?sort=-created_at,name

规则:
  - 前缀表示升序(默认)
  + 或无前缀表示升序
  - 多字段排序用逗号分隔

2.4 错误响应格式

api-designrules/common/patterns.md 共同定义了标准的错误响应格式:

{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数校验失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确"
      },
      {
        "field": "password",
        "message": "密码长度至少 8 位"
      }
    ]
  }
}

错误码分类

错误码前缀含义示例
AUTH_*认证/授权AUTH_TOKEN_EXPIRED
VALIDATION_*参数校验VALIDATION_ERROR
RESOURCE_*资源相关RESOURCE_NOT_FOUND
BUSINESS_*业务规则BUSINESS_INSUFFICIENT_BALANCE
SYSTEM_*系统错误SYSTEM_INTERNAL_ERROR

2.5 API Response 信封格式

rules/common/patterns.md 定义了统一的响应信封:

// 成功响应(单个资源)
{
  "success": true,
  "data": {
    "id": "123",
    "name": "Alice",
    "email": "alice@example.com"
  },
  "error": null
}

// 成功响应(列表 + 分页元数据)
{
  "success": true,
  "data": [
    { "id": "1", "name": "Alice" },
    { "id": "2", "name": "Bob" }
  ],
  "error": null,
  "metadata": {
    "total": 150,
    "page": 1,
    "limit": 20,
    "totalPages": 8
  }
}

2.6 版本控制

URL 路径版本(推荐):
  /api/v1/users
  /api/v2/users

Header 版本:
  Accept: application/vnd.myapp.v2+json

规则:
  - 新版本不删除旧字段,只新增字段(向后兼容)
  - 旧版本至少维护 6 个月
  - 用 Sunset 头通知即将废弃

2.7 速率限制

响应头示例:
  X-RateLimit-Limit: 100         时间窗口内最大请求数
  X-RateLimit-Remaining: 67      剩余请求数
  X-RateLimit-Reset: 1620000000  重置时间(Unix 时间戳)

超限响应:
  HTTP/1.1 429 Too Many Requests
  Retry-After: 30

三、框架专用 API Skill

3.1 各框架的 API 层实现

不同框架有不同的 API 层组织方式,但 api-design 的规范是通用的:

框架SkillAPI 层关键概念
Djangodjango-patternsViewSet、Serializer、Router(DRF)
Spring Bootspringboot-patterns@RestController、@RequestMapping、ResponseEntity
Laravellaravel-patternsRoute、Controller、Resource、FormRequest
NestJSnestjs-patterns@Controller、DTO、ValidationPipe
Ktorkotlin-ktor-patterns路由 DSL、ContentNegotiation 插件

3.2 Django REST Framework 示例

# serializers.py
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'name', 'email', 'created_at']
        read_only_fields = ['id', 'created_at']

# views.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]
    pagination_class = PageNumberPagination
    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_fields = ['status', 'role']
    ordering_fields = ['created_at', 'name']

3.3 Spring Boot 示例

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping
    public ResponseEntity<ApiResponse<Page<UserDTO>>> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int limit) {
        Page<UserDTO> users = userService.findAll(PageRequest.of(page, limit));
        return ResponseEntity.ok(ApiResponse.success(users));
    }

    @PostMapping
    public ResponseEntity<ApiResponse<UserDTO>> create(
            @Valid @RequestBody CreateUserRequest request) {
        UserDTO user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(ApiResponse.success(user));
    }
}

3.4 Ktor 路由 DSL 示例

fun Route.userRoutes(userService: UserService) {
    route("/api/v1/users") {
        get {
            val page = call.parameters["page"]?.toIntOrNull() ?: 1
            val limit = call.parameters["limit"]?.toIntOrNull() ?: 20
            val users = userService.findAll(page, limit)
            call.respond(ApiResponse.success(users))
        }

        post {
            val request = call.receive<CreateUserRequest>()
            val user = userService.create(request)
            call.respond(HttpStatusCode.Created, ApiResponse.success(user))
        }
    }
}

四、后端四层架构

4.1 backend-patterns Skill

backend-patterns Skill 定义了标准的后端四层架构:

┌──────────────────────────────────────┐
│           Controller 层              │
│  接收请求 → 参数校验 → 调用 Service  │
│  不含业务逻辑                         │
├──────────────────────────────────────┤
│           Service 层                 │
│  业务逻辑 → 编排 Repository 调用      │
│  事务管理在这一层                      │
├──────────────────────────────────────┤
│           Repository 层              │
│  数据访问 → CRUD 操作                 │
│  封装 SQL/ORM 查询                    │
├──────────────────────────────────────┤
│           Database 层                │
│  PostgreSQL / MySQL / MongoDB        │
└──────────────────────────────────────┘

4.2 各层职责边界

可以做不可以做
Controller参数校验、请求/响应格式转换、调用 Service直接操作数据库、包含业务逻辑
Service业务逻辑、事务管理、调用多个 Repository直接操作 HTTP 对象、直接写 SQL
RepositoryCRUD 操作、查询构建、缓存包含业务逻辑、操作 HTTP 对象
Database存储数据、执行 SQL、索引包含应用逻辑

4.3 为什么要分层?

不分层的问题:

Controller 直接操作数据库
  → 同一个查询在多个 Controller 中重复
  → 换数据库需要改所有 Controller
  → 无法单独测试业务逻辑

分层的好处:

Controller 只负责 HTTP 相关逻辑
  → Service 可以被多个 Controller 复用
  → Repository 可以被多个 Service 复用
  → 换数据库只改 Repository 层
  → 每层可以独立测试

五、API 设计实战

5.1 设计案例:Task 资源

让我们为一个 Task(任务)资源设计完整的 RESTful API:

资源定义

{
  "id": "uuid",
  "title": "string (required, 1-255 chars)",
  "description": "string (optional, max 2000 chars)",
  "status": "enum: pending | in_progress | completed | cancelled",
  "priority": "enum: low | medium | high | urgent",
  "assignee_id": "uuid (optional)",
  "due_date": "ISO 8601 datetime (optional)",
  "created_at": "ISO 8601 datetime (read-only)",
  "updated_at": "ISO 8601 datetime (read-only)"
}

端点设计

方法URL描述状态码
GET/api/v1/tasks查询任务列表200
GET/api/v1/tasks/:id查询单个任务200 / 404
POST/api/v1/tasks创建任务201 / 400
PATCH/api/v1/tasks/:id更新任务200 / 404 / 400
DELETE/api/v1/tasks/:id删除任务204 / 404

列表查询参数

GET /api/v1/tasks?status=pending&priority=high&assignee_id=uuid&sort=-due_date&page=1&limit=20

创建请求

POST /api/v1/tasks
Content-Type: application/json

{
  "title": "实现用户注册功能",
  "description": "包含邮箱验证和密码强度校验",
  "priority": "high",
  "assignee_id": "550e8400-e29b-41d4-a716-446655440000",
  "due_date": "2026-04-15T23:59:59Z"
}

成功响应

HTTP/1.1 201 Created
Location: /api/v1/tasks/660e8400-e29b-41d4-a716-446655440001

{
  "success": true,
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "title": "实现用户注册功能",
    "description": "包含邮箱验证和密码强度校验",
    "status": "pending",
    "priority": "high",
    "assignee_id": "550e8400-e29b-41d4-a716-446655440000",
    "due_date": "2026-04-15T23:59:59Z",
    "created_at": "2026-04-08T10:30:00Z",
    "updated_at": "2026-04-08T10:30:00Z"
  },
  "error": null
}

校验失败响应

HTTP/1.1 400 Bad Request

{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数校验失败",
    "details": [
      { "field": "title", "message": "标题不能为空" },
      { "field": "priority", "message": "优先级必须是 low/medium/high/urgent 之一" }
    ]
  }
}

六、本课练习

练习 1:查看 api-design Skill(10 分钟)

ls skills/api-design/

回答问题:

  • Skill 中关于版本控制推荐了哪种方式?
  • 对于批量操作(如批量删除),推荐的端点设计是什么?

练习 2:为 Task 资源设计完整 API(25 分钟)

这是本课最重要的练习。

在第五节设计案例的基础上,补充以下内容:

  1. 状态转换接口:如何设计"将任务标记为完成"的接口?用 PATCH 还是专用端点?
  2. 批量操作:如何设计"批量删除任务"接口?
  3. 子资源:如何设计"任务评论"接口?写出完整的端点列表。
  4. 错误处理:为每个端点列出可能的错误状态码和错误码。

练习 3:审查现有 API(15 分钟)

选择你当前项目的一个 API,用 api-design Skill 的规范审查它:

  • 资源命名是否使用复数名词?
  • 状态码使用是否准确?
  • 错误响应是否包含足够的信息?
  • 分页参数格式是否一致?

练习 4(选做):思考题

REST API 和 GraphQL 各有什么优缺点?在什么场景下你会选择 GraphQL 而不是 REST?ECC 的 api-design Skill 的哪些原则(如错误格式、版本控制)在 GraphQL 中仍然适用?


七、本课小结

你应该记住的内容
资源命名复数名词 + HTTP 方法表达动作 + 嵌套不超过两层
状态码401 是认证,403 是授权,422 是业务规则
响应格式信封格式:success + data + error + metadata
四层架构Controller → Service → Repository → Database
框架 SkillDjango/Spring/NestJS/Ktor 各有专用 API Skill

八、下节预告

第 22 课:软件架构 — 六边形、微服务与决策记录

下节课我们将从 API 设计上升到系统架构层面。你将学习六边形架构(Ports & Adapters)、架构决策记录(ADR),以及 ECC 的 architect Agent 如何辅助架构决策。

预习建议:提前浏览 skills/hexagonal-architectureskills/architecture-decision-records 目录。