343 lines
7.4 KiB
Markdown
343 lines
7.4 KiB
Markdown
---
|
||
name: rest-design-patterns
|
||
description: REST 设计模式参考手册
|
||
metadata:
|
||
type: reference
|
||
---
|
||
|
||
# REST 设计模式
|
||
|
||
## 面向资源的架构
|
||
|
||
REST API 围绕资源(而非动作)构建。资源是 API 中的名词。
|
||
|
||
### 资源标识
|
||
|
||
**良好的资源 URI:**
|
||
```
|
||
GET /users # 集合
|
||
GET /users/{id} # 单个资源
|
||
GET /users/{id}/orders # 嵌套集合
|
||
POST /users # 创建资源
|
||
PUT /users/{id} # 替换资源
|
||
PATCH /users/{id} # 更新资源
|
||
DELETE /users/{id} # 删除资源
|
||
```
|
||
|
||
**不良的资源 URI:**
|
||
```
|
||
POST /getUser # URI 中包含动词
|
||
POST /createUser # URI 中包含动词
|
||
GET /user?action=delete # 将动作作为查询参数
|
||
```
|
||
|
||
### 资源命名约定
|
||
|
||
- 集合使用复数名词:`/users`、`/orders`、`/products`
|
||
- 使用小写字母和连字符以提高可读性:`/shipping-addresses`
|
||
- 避免深层嵌套(最多 2-3 层):`/users/{id}/orders/{orderId}`
|
||
- 使用查询参数进行过滤:`/users?status=active&role=admin`
|
||
|
||
## HTTP 方法语义
|
||
|
||
### 安全方法与幂等方法
|
||
|
||
| 方法 | 安全 | 幂等 | 使用场景 |
|
||
|--------|------|------------|----------|
|
||
| GET | 是 | 是 | 获取资源 |
|
||
| POST | 否 | 否 | 创建资源、非幂等操作 |
|
||
| PUT | 否 | 是 | 替换整个资源 |
|
||
| PATCH | 否 | 否 | 部分更新 |
|
||
| DELETE | 否 | 是 | 删除资源 |
|
||
| HEAD | 是 | 是 | 仅获取元数据 |
|
||
| OPTIONS | 是 | 是 | 获取允许的方法 |
|
||
|
||
### 方法使用
|
||
|
||
**GET —— 获取资源**
|
||
```http
|
||
GET /users/123
|
||
Accept: application/json
|
||
|
||
响应:200 OK
|
||
{
|
||
"id": 123,
|
||
"name": "John Doe",
|
||
"email": "john@example.com",
|
||
"created_at": "2024-01-15T10:30:00Z"
|
||
}
|
||
```
|
||
|
||
**POST —— 创建资源**
|
||
```http
|
||
POST /users
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "Jane Smith",
|
||
"email": "jane@example.com"
|
||
}
|
||
|
||
响应:201 Created
|
||
Location: /users/124
|
||
{
|
||
"id": 124,
|
||
"name": "Jane Smith",
|
||
"email": "jane@example.com",
|
||
"created_at": "2024-01-16T14:20:00Z"
|
||
}
|
||
```
|
||
|
||
**PUT —— 替换资源**
|
||
```http
|
||
PUT /users/123
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "John Doe Updated",
|
||
"email": "john.new@example.com"
|
||
}
|
||
|
||
响应:200 OK
|
||
{
|
||
"id": 123,
|
||
"name": "John Doe Updated",
|
||
"email": "john.new@example.com",
|
||
"updated_at": "2024-01-17T09:15:00Z"
|
||
}
|
||
```
|
||
|
||
**PATCH —— 部分更新**
|
||
```http
|
||
PATCH /users/123
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"email": "john.updated@example.com"
|
||
}
|
||
|
||
响应:200 OK
|
||
{
|
||
"id": 123,
|
||
"name": "John Doe",
|
||
"email": "john.updated@example.com",
|
||
"updated_at": "2024-01-17T10:00:00Z"
|
||
}
|
||
```
|
||
|
||
**DELETE —— 删除资源**
|
||
```http
|
||
DELETE /users/123
|
||
|
||
响应:204 No Content
|
||
```
|
||
|
||
## HTTP 状态码
|
||
|
||
### 成功状态码(2xx)
|
||
|
||
- **200 OK** —— 请求成功(GET、PUT、PATCH)
|
||
- **201 Created** —— 资源已创建(POST),需包含 Location 头部
|
||
- **202 Accepted** —— 请求已接受,待异步处理
|
||
- **204 No Content** —— 成功但无响应体(DELETE)
|
||
|
||
### 重定向(3xx)
|
||
|
||
- **301 Moved Permanently** —— 资源已永久迁移
|
||
- **302 Found** —— 临时重定向
|
||
- **304 Not Modified** —— 缓存版本仍然有效
|
||
|
||
### 客户端错误(4xx)
|
||
|
||
- **400 Bad Request** —— 请求语法无效或校验错误
|
||
- **401 Unauthorized** —— 需要认证或认证失败
|
||
- **403 Forbidden** —— 已认证但无权限
|
||
- **404 Not Found** —— 资源不存在
|
||
- **405 Method Not Allowed** —— 该 HTTP 方法不被资源支持
|
||
- **409 Conflict** —— 请求与当前状态冲突(例如重复)
|
||
- **422 Unprocessable Entity** —— 语法正确但语义错误
|
||
- **429 Too Many Requests** —— 请求频率超限
|
||
|
||
### 服务端错误(5xx)
|
||
|
||
- **500 Internal Server Error** —— 服务器意外错误
|
||
- **502 Bad Gateway** —— 上游服务器返回无效响应
|
||
- **503 Service Unavailable** —— 服务器暂时不可用
|
||
- **504 Gateway Timeout** —— 上游服务器超时
|
||
|
||
## HATEOAS(超媒体)
|
||
|
||
### 超媒体驱动的 API
|
||
|
||
包含指向相关资源和可用操作的链接:
|
||
|
||
```json
|
||
{
|
||
"id": 123,
|
||
"name": "John Doe",
|
||
"email": "john@example.com",
|
||
"_links": {
|
||
"self": { "href": "/users/123" },
|
||
"orders": { "href": "/users/123/orders" },
|
||
"update": { "href": "/users/123", "method": "PATCH" },
|
||
"delete": { "href": "/users/123", "method": "DELETE" }
|
||
}
|
||
}
|
||
```
|
||
|
||
### HAL(超文本应用语言)
|
||
|
||
```json
|
||
{
|
||
"id": 123,
|
||
"name": "John Doe",
|
||
"_links": {
|
||
"self": { "href": "/users/123" }
|
||
},
|
||
"_embedded": {
|
||
"orders": [
|
||
{
|
||
"id": 456,
|
||
"total": 99.99,
|
||
"_links": {
|
||
"self": { "href": "/orders/456" }
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
## 内容协商
|
||
|
||
### Accept 头部
|
||
|
||
```http
|
||
GET /users/123
|
||
Accept: application/json
|
||
|
||
GET /users/123
|
||
Accept: application/xml
|
||
|
||
GET /users/123
|
||
Accept: application/hal+json
|
||
```
|
||
|
||
### 响应 Content-Type
|
||
|
||
```http
|
||
Content-Type: application/json; charset=utf-8
|
||
Content-Type: application/problem+json
|
||
Content-Type: application/hal+json
|
||
```
|
||
|
||
## 幂等性
|
||
|
||
### 幂等操作
|
||
|
||
**PUT —— 始终幂等:**
|
||
多次相同的 PUT 请求与单次请求产生相同的结果。
|
||
|
||
**DELETE —— 幂等:**
|
||
第一次 DELETE 返回 204,后续 DELETE 返回 404(最终状态相同)。
|
||
|
||
**POST —— 默认不幂等:**
|
||
使用 `Idempotency-Key` 头部实现幂等 POST:
|
||
|
||
```http
|
||
POST /payments
|
||
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"amount": 100.00,
|
||
"currency": "USD"
|
||
}
|
||
```
|
||
|
||
服务端存储幂等键,对重复请求返回相同响应。
|
||
|
||
## 缓存控制
|
||
|
||
### 缓存头部
|
||
|
||
```http
|
||
Cache-Control: public, max-age=3600
|
||
Cache-Control: private, no-cache
|
||
Cache-Control: no-store
|
||
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||
Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT
|
||
```
|
||
|
||
### 条件请求
|
||
|
||
```http
|
||
GET /users/123
|
||
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||
|
||
响应:304 Not Modified
|
||
```
|
||
|
||
```http
|
||
PUT /users/123
|
||
If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"name": "Updated Name"
|
||
}
|
||
|
||
响应:412 Precondition Failed(如果 ETag 不匹配)
|
||
```
|
||
|
||
## URI 模式
|
||
|
||
### 一致的 URI 结构
|
||
|
||
```
|
||
/{version}/{resource}
|
||
/{version}/{resource}/{id}
|
||
/{version}/{resource}/{id}/{sub-resource}
|
||
/{version}/{resource}/{id}/{sub-resource}/{sub-id}
|
||
```
|
||
|
||
### 查询参数
|
||
|
||
**过滤:**
|
||
```
|
||
GET /users?status=active&role=admin
|
||
GET /products?category=electronics&price_min=100&price_max=500
|
||
```
|
||
|
||
**排序:**
|
||
```
|
||
GET /users?sort=created_at
|
||
GET /users?sort=-created_at # 降序
|
||
GET /users?sort=name,created_at # 多字段
|
||
```
|
||
|
||
**字段选择:**
|
||
```
|
||
GET /users?fields=id,name,email
|
||
GET /users?exclude=password,social_security_number
|
||
```
|
||
|
||
**搜索:**
|
||
```
|
||
GET /users?q=john
|
||
GET /products?search=laptop
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
1. **使用名词,而非动词** —— 资源是名词,方法是动词
|
||
2. **集合使用复数** —— 使用 `/users` 而非 `/user`
|
||
3. **命名保持一致** —— 选择 snake_case 或 camelCase 并坚持使用
|
||
4. **使用正确的状态码** —— 选用恰当的 HTTP 状态码
|
||
5. **包含元数据** —— 在响应中提供分页、过滤、排序信息
|
||
6. **对 API 进行版本控制** —— 从第一天起就规划演进
|
||
7. **记录所有内容** —— OpenAPI 规范、示例、错误码
|
||
8. **默认启用安全** —— HTTPS、认证、速率限制
|
||
9. **支持过滤** —— 让客户端精确获取所需数据
|
||
10. **实现 HATEOAS** —— 使 API 具备自文档和自发现能力
|