chore: import zh skill fullstack-dev
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
---
|
||||
name: fullstack-dev-api-design
|
||||
description: "API 设计模式与最佳实践。适用于创建端点、选择方法/状态码、实现分页或编写 OpenAPI 规范。可避免常见的 REST/GraphQL/gRPC 错误。"
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "2.0.0"
|
||||
sources:
|
||||
- Microsoft REST API Guidelines
|
||||
- Google API Design Guide
|
||||
- Zalando RESTful API Guidelines
|
||||
- JSON:API Specification
|
||||
- RFC 9457 (Problem Details for HTTP APIs)
|
||||
- RFC 9110 (HTTP Semantics)
|
||||
---
|
||||
|
||||
# API 设计指南
|
||||
|
||||
面向后端与全栈工程师的框架无关 API 设计指南。涵盖 10 大类共 50 余条规则,按影响程度排序。覆盖 REST、GraphQL 和 gRPC。
|
||||
|
||||
## 适用范围
|
||||
|
||||
**使用此技能的场景:**
|
||||
- 设计新 API 或添加端点
|
||||
- 审查 API 拉取请求
|
||||
- 在 REST / GraphQL / gRPC 之间做选择
|
||||
- 编写 OpenAPI 规范
|
||||
- 迁移或对现有 API 进行版本管理
|
||||
|
||||
**不适用的场景:**
|
||||
- 框架特定的实现细节(请使用对应框架的技能/文档)
|
||||
- 前端数据获取模式(请使用 React Query / SWR 文档)
|
||||
- 认证实现细节(请使用认证库的文档)
|
||||
- 数据库 schema 设计(→ `database-schema-design`)
|
||||
|
||||
## 所需上下文
|
||||
|
||||
在应用此技能前,请收集以下信息:
|
||||
|
||||
| 必填项 | 可选项 |
|
||||
|----------|----------|
|
||||
| 目标消费者(浏览器、移动端、服务) | 项目中已有的 API 约定 |
|
||||
| 预期请求量(RPS 预估) | 当前的 OpenAPI / Swagger 规范 |
|
||||
| 认证方式(JWT、API Key、OAuth) | 限流需求 |
|
||||
| 数据模型 / 领域实体 | 缓存策略 |
|
||||
|
||||
---
|
||||
|
||||
## 快速开始清单
|
||||
|
||||
新建 API 端点?写代码前逐一检查:
|
||||
|
||||
- [ ] 资源命名为**复数名词**(`/orders`,而不是 `/getOrders`)
|
||||
- [ ] URL 使用**短横线命名**,请求体字段使用**驼峰命名**
|
||||
- [ ] 使用正确的 **HTTP 方法**(GET=读取,POST=创建,PUT=全量替换,PATCH=部分更新,DELETE=删除)
|
||||
- [ ] 使用正确的**状态码**(201 Created、422 Validation、404 Not Found……)
|
||||
- [ ] 错误响应遵循 **RFC 9457** 格式
|
||||
- [ ] 所有列表端点实现**分页**(默认 20,最多 100)
|
||||
- [ ] 要求**认证**(Bearer Token,而非查询参数)
|
||||
- [ ] 响应头中包含**请求 ID**(`X-Request-Id`)
|
||||
- [ ] 包含**限流**响应头
|
||||
- [ ] 端点已在 **OpenAPI 规范**中记录
|
||||
|
||||
---
|
||||
|
||||
## 快速导航
|
||||
|
||||
| 需要…… | 跳转至 |
|
||||
|----------|---------|
|
||||
| 命名资源 URL | [1. 资源建模](#1-资源建模关键) |
|
||||
| 选择 HTTP 方法与状态码 | [3. HTTP 方法与状态码](#3-http-方法与状态码关键) |
|
||||
| 格式化错误响应 | [4. 错误处理](#4-错误处理高优先级) |
|
||||
| 添加分页或过滤 | [6. 分页与过滤](#6-分页与过滤高优先级) |
|
||||
| 选择 API 风格(REST vs GraphQL vs gRPC) | [10. API 风格决策](#10-api-风格决策树) |
|
||||
| 对现有 API 进行版本管理 | [7. 版本管理](#7-版本管理中高优先级) |
|
||||
| 避免常见错误 | [反模式清单](#反模式清单) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 资源建模(关键)
|
||||
|
||||
### 核心规则
|
||||
|
||||
```
|
||||
✅ /users — 复数名词
|
||||
✅ /users/{id}/orders — 1 层嵌套
|
||||
✅ /reviews?orderId={oid} — 使用查询参数展平深层嵌套
|
||||
|
||||
❌ /getUsers — URL 中包含动词
|
||||
❌ /user — 单数
|
||||
❌ /users/{uid}/orders/{oid}/items/{iid}/reviews — 超过 3 层嵌套
|
||||
```
|
||||
|
||||
**最大嵌套层级:2 层。** 超过此限制时,提升为顶层资源并通过过滤参数查询。
|
||||
|
||||
### 领域对齐
|
||||
|
||||
资源映射到**领域概念**,而非数据库表:
|
||||
|
||||
```
|
||||
✅ /checkout-sessions (领域聚合)
|
||||
✅ /shipping-labels (领域概念)
|
||||
|
||||
❌ /tbl_order_header (数据库表泄露)
|
||||
❌ /join_user_role (内部 schema 泄露)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. URL 与命名(关键)
|
||||
|
||||
| 上下文 | 规范 | 示例 |
|
||||
|---------|-----------|---------|
|
||||
| URL 路径 | 短横线命名 | `/order-items` |
|
||||
| JSON 请求体字段 | 驼峰命名 | `{ "firstName": "Jane" }` |
|
||||
| 查询参数 | 驼峰命名或下划线命名(保持一致) | `?sortBy=createdAt` |
|
||||
| 请求头 | 连字符大写命名 | `X-Request-Id` |
|
||||
|
||||
**Python 例外:** 如果整个技术栈都是 Python/下划线命名,可以在 JSON 中使用 `snake_case`——但必须在**所有端点间保持一致**。
|
||||
|
||||
```
|
||||
✅ GET /users ❌ GET /users/
|
||||
✅ GET /reports/annual ❌ GET /reports/annual.json
|
||||
✅ POST /users ❌ POST /users/create
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. HTTP 方法与状态码(关键)
|
||||
|
||||
### 方法语义
|
||||
|
||||
| 方法 | 语义 | 幂等 | 安全 | 请求体 |
|
||||
|--------|-----------|-----------|------|-------------|
|
||||
| GET | 读取 | ✅ | ✅ | ❌ 从不 |
|
||||
| POST | 创建 / 动作 | ❌ | ❌ | ✅ 始终 |
|
||||
| PUT | 全量替换 | ✅ | ❌ | ✅ 始终 |
|
||||
| PATCH | 部分更新 | ❌* | ❌ | ✅ 始终 |
|
||||
| DELETE | 删除 | ✅ | ❌ | ❌ 极少 |
|
||||
|
||||
### 状态码快速参考
|
||||
|
||||
**成功类:**
|
||||
|
||||
| 状态码 | 使用时机 | 响应体 |
|
||||
|------|------|--------------|
|
||||
| 200 OK | GET、PUT、PATCH 成功 | 资源 / 结果 |
|
||||
| 201 Created | POST 创建了资源 | 已创建的资源 + `Location` 请求头 |
|
||||
| 202 Accepted | 异步操作已启动 | 任务 ID / 状态 URL |
|
||||
| 204 No Content | DELETE 成功、PUT 无请求体 | 无 |
|
||||
|
||||
**客户端错误类:**
|
||||
|
||||
| 状态码 | 使用时机 | 关键区别 |
|
||||
|------|------|-----------------|
|
||||
| 400 Bad Request | 语法格式错误 | 无法解析 |
|
||||
| 401 Unauthorized | 缺少或无效的认证凭据 | "你是谁?" |
|
||||
| 403 Forbidden | 已认证但无权限 | "我认识你,但不行" |
|
||||
| 404 Not Found | 资源不存在 | 也可用于隐藏 403 |
|
||||
| 409 Conflict | 重复、版本不匹配 | 状态冲突 |
|
||||
| 422 Unprocessable | 语法正确但验证失败 | 语义错误 |
|
||||
| 429 Too Many Requests | 触发限流 | 包含 `Retry-After` |
|
||||
|
||||
**服务端错误类:** 500(意外错误)、502(上游失败)、503(过载)、504(上游超时)
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误处理(高优先级)
|
||||
|
||||
### 标准错误格式(RFC 9457)
|
||||
|
||||
所有错误响应均使用此格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://api.example.com/errors/insufficient-funds",
|
||||
"title": "余额不足",
|
||||
"status": 422,
|
||||
"detail": "账户余额 $10.00 低于提现金额 $50.00。",
|
||||
"instance": "/transactions/txn_abc123",
|
||||
"request_id": "req_7f3a8b2c",
|
||||
"errors": [
|
||||
{ "field": "amount", "message": "超出余额", "code": "INSUFFICIENT_BALANCE" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 多语言实现
|
||||
|
||||
**TypeScript(Express):**
|
||||
```typescript
|
||||
class AppError extends Error {
|
||||
constructor(
|
||||
public readonly title: string,
|
||||
public readonly status: number,
|
||||
public readonly detail: string,
|
||||
public readonly code: string,
|
||||
) { super(detail); }
|
||||
}
|
||||
|
||||
// 中间件
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.status).json({
|
||||
type: `https://api.example.com/errors/${err.code}`,
|
||||
title: err.title, status: err.status,
|
||||
detail: err.detail, request_id: req.id,
|
||||
});
|
||||
}
|
||||
res.status(500).json({ title: '内部错误', status: 500, request_id: req.id });
|
||||
});
|
||||
```
|
||||
|
||||
**Python(FastAPI):**
|
||||
```python
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
class AppError(Exception):
|
||||
def __init__(self, title: str, status: int, detail: str, code: str):
|
||||
self.title, self.status, self.detail, self.code = title, status, detail, code
|
||||
|
||||
@app.exception_handler(AppError)
|
||||
async def app_error_handler(request: Request, exc: AppError):
|
||||
return JSONResponse(status_code=exc.status, content={
|
||||
"type": f"https://api.example.com/errors/{exc.code}",
|
||||
"title": exc.title, "status": exc.status,
|
||||
"detail": exc.detail, "request_id": request.state.request_id,
|
||||
})
|
||||
```
|
||||
|
||||
### 铁律
|
||||
|
||||
```
|
||||
✅ 所有错误均返回 RFC 9457 错误格式
|
||||
✅ 每个错误响应都包含 request_id
|
||||
✅ 字段级验证错误在 `errors` 数组中返回
|
||||
|
||||
❌ 绝不在生产环境暴露堆栈信息
|
||||
❌ 绝不为错误返回 200 状态码
|
||||
❌ 绝不静默吞掉错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 认证与授权(高优先级)
|
||||
|
||||
```
|
||||
✅ Authorization: Bearer eyJhbGci... (请求头)
|
||||
❌ GET /users?token=eyJhbGci... (URL——会出现在日志中)
|
||||
|
||||
✅ 401 → "你是谁?" (缺少或无效的凭据)
|
||||
✅ 403 → "你不能这样做" (已认证,无权限)
|
||||
✅ 404 → 隐藏资源存在性 (需要时用来替代 403)
|
||||
```
|
||||
|
||||
**限流响应头(始终包含):**
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 42
|
||||
X-RateLimit-Reset: 1625097600
|
||||
Retry-After: 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 分页与过滤(高优先级)
|
||||
|
||||
### 游标分页 vs 偏移分页
|
||||
|
||||
| 策略 | 适用场景 | 优点 | 缺点 |
|
||||
|----------|------|------|------|
|
||||
| **游标分页**(推荐) | 大型/动态数据集 | 数据一致,无跳变 | 无法跳转到第 N 页 |
|
||||
| **偏移分页** | 小型/稳定数据集、管理后台 | 简单,支持页面跳转 | 插入/删除时数据漂移 |
|
||||
|
||||
**游标分页响应:**
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
|
||||
}
|
||||
```
|
||||
|
||||
**偏移分页响应:**
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": { "page": 3, "per_page": 20, "total": 256, "total_pages": 13 }
|
||||
}
|
||||
```
|
||||
|
||||
**始终执行:** 默认 20 条,最多 100 条。
|
||||
|
||||
### 标准过滤模式
|
||||
|
||||
```
|
||||
GET /orders?status=shipped&created_after=2025-01-01&sort=-created_at&fields=id,status
|
||||
```
|
||||
|
||||
| 模式 | 规范 |
|
||||
|---------|-----------|
|
||||
| 精确匹配 | `?status=shipped` |
|
||||
| 范围查询 | `?price_gte=10&price_lte=100` |
|
||||
| 日期范围 | `?created_after=2025-01-01&created_before=2025-12-31` |
|
||||
| 排序 | `?sort=field`(升序)、`?sort=-field`(降序) |
|
||||
| 稀疏字段 | `?fields=id,name,email` |
|
||||
| 搜索 | `?q=搜索+关键词` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 版本管理(中高优先级)
|
||||
|
||||
| 策略 | 格式 | 最佳适用 |
|
||||
|----------|--------|----------|
|
||||
| **URL 路径**(推荐) | `/v1/users` | 公开 API |
|
||||
| **请求头** | `Api-Version: 2` | 内部 API |
|
||||
| **查询参数** | `?version=2` | 遗留系统(避免使用) |
|
||||
|
||||
**非破坏性变更(无需升级版本号):** 新增可选的响应字段、新增端点、新增可选参数。
|
||||
|
||||
**破坏性变更(需新版本):** 删除/重命名字段、更改类型、收紧验证规则、删除端点。
|
||||
|
||||
**弃用响应头:**
|
||||
```
|
||||
Sunset: Sat, 01 Mar 2026 00:00:00 GMT
|
||||
Deprecation: true
|
||||
Link: <https://api.example.com/v2/users>; rel="successor-version"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 请求/响应设计(中优先级)
|
||||
|
||||
### 统一的响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"data": { "id": "ord_123", "status": "pending", "total": 99.50 },
|
||||
"meta": { "request_id": "req_abc123", "timestamp": "2025-06-15T10:30:00Z" }
|
||||
}
|
||||
```
|
||||
|
||||
### 关键规则
|
||||
|
||||
| 规则 | 正确 | 错误 |
|
||||
|------|---------|-------|
|
||||
| 时间戳 | `"2025-06-15T10:30:00Z"`(ISO 8601) | `"06/15/2025"` 或 `1718447400` |
|
||||
| 公开 ID | UUID `"550e8400-..."` | 自增 `42` |
|
||||
| Null 与缺省(PATCH) | `{ "nickname": null }` = 清空字段 | 缺省字段 = 不做更改 |
|
||||
| HATEOAS(公开 API) | `"links": { "cancel": "/orders/123/cancel" }` | 无可发现性 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 文档——OpenAPI(中优先级)
|
||||
|
||||
**设计优先的工作流程:**
|
||||
|
||||
```
|
||||
1. 编写 OpenAPI 3.1 规范
|
||||
2. 与相关方审查规范
|
||||
3. 生成服务端桩代码 + 客户端 SDK
|
||||
4. 实现处理器
|
||||
5. 在 CI 中对照规范验证响应
|
||||
```
|
||||
|
||||
每个端点均需记录:摘要、所有参数、请求体及示例、所有响应码及 schema、认证要求。
|
||||
|
||||
---
|
||||
|
||||
## 10. API 风格决策树
|
||||
|
||||
```
|
||||
什么样的 API?
|
||||
│
|
||||
├─ 浏览器 + 移动端客户端,需要灵活查询
|
||||
│ └─ GraphQL
|
||||
│ 规则:DataLoader(避免 N+1)、深度限制 ≤7、Relay 分页
|
||||
│
|
||||
├─ 标准 CRUD,面向公开消费者,缓存很重要
|
||||
│ └─ REST(本指南)
|
||||
│ 规则:资源、HTTP 方法、状态码、OpenAPI
|
||||
│
|
||||
├─ 服务间通信,高吞吐量,强类型
|
||||
│ └─ gRPC
|
||||
│ 规则:Protobuf schema、大数据流式传输、截止时间
|
||||
│
|
||||
├─ 全栈 TypeScript,同一团队拥有客户端和服务端
|
||||
│ └─ tRPC
|
||||
│ 规则:共享类型,无需代码生成
|
||||
│
|
||||
└─ 实时双向通信
|
||||
└─ WebSocket / SSE
|
||||
规则:心跳、重连、消息排序
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反模式清单
|
||||
|
||||
| # | ❌ 不要这样做 | ✅ 应该这样做 |
|
||||
|---|---------|--------------|
|
||||
| 1 | URL 中包含动词(`/getUser`) | HTTP 方法 + 名词资源 |
|
||||
| 2 | 错误时返回 200 | 正确的 4xx/5xx 状态码 |
|
||||
| 3 | 混合使用多种命名风格 | 每个上下文保持一种规范 |
|
||||
| 4 | 暴露数据库 ID | 使用 UUID 作为公开标识符 |
|
||||
| 5 | 列表不进行分页 | 始终分页(默认 20 条) |
|
||||
| 6 | 静默吞掉错误 | 结构化的 RFC 9457 错误 |
|
||||
| 7 | Token 放在 URL 查询参数中 | Authorization 请求头 |
|
||||
| 8 | 深层嵌套(3 层以上) | 使用查询参数展平 |
|
||||
| 9 | 破坏性变更不升级版本 | 保持兼容或使用版本管理 |
|
||||
| 10 | 不设限流 | 实现限流并通过请求头告知 |
|
||||
| 11 | 无请求 ID | 每个响应都包含 `X-Request-Id` |
|
||||
| 12 | 生产环境暴露堆栈信息 | 安全的错误消息 + 内部日志 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题 1:"这应该是新资源还是子资源?"
|
||||
|
||||
**现象:** URL 路径不断增长(`/users/{id}/orders/{id}/items/{id}/reviews`)
|
||||
|
||||
**规则:** 如果子实体本身有意义,则提升为顶层资源。如果它只在父级上下文中存在,则保持嵌套(最多 2 层)。
|
||||
|
||||
```
|
||||
/reviews?orderId=123 ✅ (reviews 独立存在)
|
||||
/orders/{id}/items ✅ (items 属于 orders,1 层)
|
||||
```
|
||||
|
||||
### 问题 2:"PUT 还是 PATCH?"
|
||||
|
||||
**现象:** 团队对更新语义无法达成一致。
|
||||
|
||||
**规则:**
|
||||
- PUT = 客户端发送**完整**资源(缺失字段 → 设为默认值/null)
|
||||
- PATCH = 客户端只发送**发生变化的字段**(缺失字段 → 保持不变)
|
||||
- 不确定时 → **PATCH**(更安全,更少意外)
|
||||
|
||||
### 问题 3:"400 还是 422?"
|
||||
|
||||
**现象:** 验证错误码不一致。
|
||||
|
||||
**规则:**
|
||||
- 400 = 完全无法解析请求(格式错误的 JSON、错误的 content-type)
|
||||
- 422 = 解析成功,但值未通过验证(无效的 email、负数数量)
|
||||
Reference in New Issue
Block a user