阶段一:RESTful基础
REST(Representational State Transfer)是Roy Fielding在2000年博士论文中提出的架构风格,而HTTP协议是REST最常用的实现载体。理解HTTP协议的语义和REST的约束条件,是设计高质量API的根基。
一个HTTP请求由四部分组成:请求行、请求头、空行、请求体。
POST /api/users HTTP/1.1 ← 请求行:方法 + URL + 协议版本
Host: api.example.com ← 请求头开始
Content-Type: application/json
Authorization: Bearer eyJhbGci...
Accept: application/json
X-Request-ID: abc123def456 ← 请求头结束
← 空行(CRLF)
{"name":"张三","email":"zhang@example.com"} ← 请求体
| 类别 | 常用头 | 说明 |
|---|---|---|
| 内容协商 | Accept, Accept-Encoding, Accept-Language | 客户端期望的响应格式 |
| 内容描述 | Content-Type, Content-Length, Content-Encoding | 请求体的格式和长度 |
| 认证授权 | Authorization, WWW-Authenticate | 身份验证凭据 |
| 缓存控制 | Cache-Control, ETag, If-None-Match | 缓存策略与验证 |
| 条件请求 | If-Modified-Since, If-Match | 条件化请求执行 |
| 连接控制 | Connection, Keep-Alive | 连接管理 |
| 自定义头 | X-Request-ID, X-RateLimit-* | 应用层扩展 |
HTTP/1.1 201 Created ← 状态行:版本 + 状态码 + 原因短语
Content-Type: application/json ← 响应头
Location: /api/users/42
X-Request-ID: abc123def456
X-RateLimit-Remaining: 99
← 空行
{"id":42,"name":"张三","email":"zhang@example.com"} ← 响应体
HTTP方法(动词)定义了对资源的操作语义。正确使用HTTP方法是RESTful设计的核心。
| 方法 | 语义 | 幂等 | 安全 | 有请求体 | 有响应体 |
|---|---|---|---|---|---|
| GET | 获取资源表示 | ✅ | ✅ | ❌ | ✅ |
| POST | 创建资源/触发处理 | ❌ | ❌ | ✅ | ✅ |
| PUT | 全量替换资源 | ✅ | ❌ | ✅ | 可选 |
| PATCH | 部分更新资源 | ❌ | ❌ | ✅ | ✅ |
| DELETE | 删除资源 | ✅ | ❌ | 可选 | 可选 |
| HEAD | 获取资源元数据 | ✅ | ✅ | ❌ | ❌ |
| OPTIONS | 查询支持的方法 | ✅ | ✅ | ❌ | ✅ |
| TRACE | 回显请求(调试用) | ✅ | ✅ | ❌ | ✅ |
// 原始资源
GET /api/users/1
{"id":1, "name":"张三", "email":"zhang@old.com", "role":"user"}
// PUT:全量替换(必须发送完整资源)
PUT /api/users/1
{"id":1, "name":"张三", "email":"zhang@new.com", "role":"user"}
// ✅ 结果:邮箱更新,其他字段不变
PUT /api/users/1
{"id":1, "name":"张三", "email":"zhang@new.com"}
// ⚠️ 结果:role字段被删除!PUT是替换不是合并
// PATCH:部分更新(只发送需要修改的字段)
PATCH /api/users/1
{"email":"zhang@new.com"}
// ✅ 结果:只有邮箱更新,其他字段不受影响
REST不是协议而是架构风格,它定义了六条约束条件:
关注点分离:客户端负责用户交互,服务器负责数据存储和处理。两者独立演进,通过统一接口通信。
// 客户端不需要知道数据如何存储
GET /api/products/42
// 客户端只关心返回的JSON,不关心后端是MySQL还是MongoDB
// 服务器不需要知道客户端如何展示
// 同一个API可以被Web、iOS、Android、CLI共同使用
每个请求必须包含所有必要信息,服务器不保存客户端会话状态。这是可伸缩性的基础。
// ❌ 有状态:服务器记住用户会话
// 请求1: POST /api/login → 服务器创建session
// 请求2: GET /api/profile → 服务器靠session识别用户
// ✅ 无状态:每个请求自带认证信息
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// 服务器从token中识别用户,无需session
响应必须明确标识是否可缓存,避免客户端重复请求不变的数据。
// 服务器响应中包含缓存信息
HTTP/1.1 200 OK
Cache-Control: max-age=3600 // 缓存1小时
ETag: "abc123" // 资源版本标识
Last-Modified: Wed, 15 Jan 2025 08:00:00 GMT
// 客户端后续请求可使用条件请求
GET /api/products
If-None-Match: "abc123"
// 如果资源未变化,服务器返回304 Not Modified(无响应体)
这是REST最核心的约束,包含四个子约束:
/api/users/42客户端不需要知道是直接连接到终端服务器还是中间代理。允许插入负载均衡器、缓存代理等中间层。
服务器可以临时扩展客户端功能,例如返回JavaScript代码由客户端执行。这是唯一可选的约束。
// rest-api.js - 完整RESTful API实现
const express = require('express');
const app = express();
app.use(express.json());
// ===== 中间件 =====
// 请求日志
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} → ${res.statusCode} (${duration}ms)`);
});
next();
});
// CORS 支持
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
// ===== 数据层 =====
const products = new Map();
let nextId = 1;
function initProducts() {
const items = [
{ name: '机械键盘', price: 599, category: '外设', stock: 120 },
{ name: '无线鼠标', price: 199, category: '外设', stock: 250 },
{ name: '4K显示器', price: 2999, category: '显示器', stock: 45 },
{ name: 'USB-C扩展坞', price: 399, category: '配件', stock: 80 },
{ name: '降噪耳机', price: 1299, category: '音频', stock: 60 },
];
items.forEach(item => {
const id = nextId++;
const product = {
id,
...item,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
products.set(id, product);
});
}
initProducts();
// ===== 响应工具 =====
function success(res, data, status = 200, meta = {}) {
res.status(status).json({
success: true,
data,
meta: { ...meta, timestamp: new Date().toISOString() },
});
}
function error(res, status, code, message, details = []) {
res.status(status).json({
success: false,
error: { code, message, details },
meta: { timestamp: new Date().toISOString() },
});
}
// ===== 路由 =====
// GET /api/products - 列表(支持过滤、排序)
app.get('/api/products', (req, res) => {
let result = [...products.values()];
// 过滤
if (req.query.category) {
result = result.filter(p => p.category === req.query.category);
}
if (req.query.minPrice) {
result = result.filter(p => p.price >= Number(req.query.minPrice));
}
if (req.query.maxPrice) {
result = result.filter(p => p.price <= Number(req.query.maxPrice));
}
if (req.query.inStock === 'true') {
result = result.filter(p => p.stock > 0);
}
// 排序
const sortBy = req.query.sortBy || 'id';
const order = req.query.order === 'desc' ? -1 : 1;
result.sort((a, b) => (a[sortBy] > b[sortBy] ? order : -order));
// 分页
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
const total = result.length;
const offset = (page - 1) * limit;
result = result.slice(offset, offset + limit);
success(res, result, 200, {
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
});
});
// GET /api/products/:id - 详情
app.get('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id);
const product = products.get(id);
if (!product) return error(res, 404, 'NOT_FOUND', `产品 ${id} 不存在`);
success(res, product);
});
// POST /api/products - 创建
app.post('/api/products', (req, res) => {
const { name, price, category, stock } = req.body;
// 验证
const errors = [];
if (!name) errors.push({ field: 'name', message: '产品名称为必填项' });
if (price === undefined) errors.push({ field: 'price', message: '价格为必填项' });
else if (price < 0) errors.push({ field: 'price', message: '价格不能为负数' });
if (stock !== undefined && stock < 0) errors.push({ field: 'stock', message: '库存不能为负数' });
if (errors.length > 0) {
return error(res, 400, 'VALIDATION_ERROR', '请求数据验证失败', errors);
}
const id = nextId++;
const product = {
id,
name,
price,
category: category || '未分类',
stock: stock || 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
products.set(id, product);
// 设置Location头(REST最佳实践)
res.location(`/api/products/${id}`);
success(res, product, 201);
});
// PUT /api/products/:id - 全量更新
app.put('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id);
const existing = products.get(id);
if (!existing) return error(res, 404, 'NOT_FOUND', `产品 ${id} 不存在`);
const { name, price, category, stock } = req.body;
if (!name || price === undefined) {
return error(res, 400, 'VALIDATION_ERROR', 'PUT请求必须包含完整资源数据');
}
const updated = {
id,
name,
price,
category: category || '未分类',
stock: stock || 0,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
products.set(id, updated);
success(res, updated);
});
// PATCH /api/products/:id - 部分更新
app.patch('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id);
const existing = products.get(id);
if (!existing) return error(res, 404, 'NOT_FOUND', `产品 ${id} 不存在`);
const allowed = ['name', 'price', 'category', 'stock'];
const updates = {};
for (const key of allowed) {
if (req.body[key] !== undefined) updates[key] = req.body[key];
}
if (Object.keys(updates).length === 0) {
return error(res, 400, 'VALIDATION_ERROR', '至少需要一个更新字段');
}
const updated = {
...existing,
...updates,
updatedAt: new Date().toISOString(),
};
products.set(id, updated);
success(res, updated);
});
// DELETE /api/products/:id - 删除
app.delete('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id);
if (!products.has(id)) return error(res, 404, 'NOT_FOUND', `产品 ${id} 不存在`);
products.delete(id);
res.status(204).end(); // 204 No Content - DELETE标准响应
});
// OPTIONS /api/products - CORS预检
app.options('/api/products', (req, res) => {
res.setHeader('Allow', 'GET, POST, OPTIONS');
res.status(204).end();
});
// HEAD /api/products - 只返回头
app.head('/api/products', (req, res) => {
res.setHeader('X-Total-Count', products.size.toString());
res.status(200).end();
});
// ===== 404兜底 =====
app.use((req, res) => {
error(res, 404, 'NOT_FOUND', `路径 ${req.method} ${req.path} 不存在`);
});
// ===== 全局错误处理 =====
app.use((err, req, res, next) => {
console.error('未捕获错误:', err);
error(res, 500, 'INTERNAL_ERROR', '服务器内部错误');
});
// ===== 启动 =====
const PORT = 3000;
app.listen(PORT, () => {
console.log(`🚀 REST API 运行在 http://localhost:${PORT}`);
console.log('接口列表:');
console.log(' GET /api/products - 产品列表');
console.log(' GET /api/products/:id - 产品详情');
console.log(' POST /api/products - 创建产品');
console.log(' PUT /api/products/:id - 全量更新');
console.log(' PATCH /api/products/:id - 部分更新');
console.log(' DELETE /api/products/:id - 删除产品');
console.log(' HEAD /api/products - 资源元数据');
console.log(' OPTIONS /api/products - 支持的方法');
});
# 安装express并启动
npm install express
node rest-api.js
# 1. GET - 获取产品列表
curl -s http://localhost:3000/api/products | jq .
{
"success": true,
"data": [
{"id":1,"name":"机械键盘","price":599,"category":"外设","stock":120,...},
{"id":2,"name":"无线鼠标","price":199,"category":"外设","stock":250,...},
...
],
"meta": {"pagination":{"page":1,"limit":20,"total":5,"totalPages":1},...}
}
# 2. POST - 创建产品
curl -s -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-d '{"name":"蓝牙音箱","price":299,"category":"音频","stock":50}' | jq .
{
"success": true,
"data": {"id":6,"name":"蓝牙音箱","price":299,"category":"音频","stock":50,...},
"meta": {...}
}
# 3. PUT - 全量更新
curl -s -X PUT http://localhost:3000/api/products/1 \
-H "Content-Type: application/json" \
-d '{"name":"机械键盘Pro","price":699,"category":"外设","stock":100}' | jq .
# 4. PATCH - 部分更新(只改价格)
curl -s -X PATCH http://localhost:3000/api/products/2 \
-H "Content-Type: application/json" \
-d '{"price":179}' | jq .
# 5. DELETE - 删除产品
curl -s -X DELETE http://localhost:3000/api/products/5 -w "\nHTTP Status: %{http_code}\n"
# 6. HEAD - 只获取元数据
curl -I http://localhost:3000/api/products
# 7. OPTIONS - 查看支持的方法
curl -X OPTIONS http://localhost:3000/api/products -v
# 8. 带过滤条件查询
curl -s "http://localhost:3000/api/products?category=外设&minPrice=100&sortBy=price&order=desc" | jq .
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC(UDP) |
| 多路复用 | ❌ 队头阻塞 | ✅ 流式复用 | ✅ 独立流 |
| 头部压缩 | ❌ | ✅ HPACK | ✅ QPACK |
| 服务器推送 | ❌ | ✅(已弃用) | ❌ |
| 连接建立 | 1-RTT | 1-RTT+TLS | 0-RTT |
| API影响 | 需合并请求减少连接 | 无需刻意合并 | 更适合弱网环境 |
一个博客系统有以下资源:文章、评论、标签、用户。请设计完整的RESTful API路由,包括:
参考:
# 文章
GET /api/articles # 列表(?tag=xxx&author=xxx)
GET /api/articles/:slug # 详情(用slug而非id更RESTful)
POST /api/articles
PUT /api/articles/:slug
PATCH /api/articles/:slug
DELETE /api/articles/:slug
# 评论(嵌套资源)
GET /api/articles/:slug/comments
POST /api/articles/:slug/comments
PATCH /api/comments/:id # 或 PUT
DELETE /api/comments/:id
# 标签
GET /api/tags
POST /api/tags
DELETE /api/tags/:name
以下场景分别应该用哪个HTTP方法?
参考:1.PATCH 2.HEAD(返回X-Comment-Count头) 3.POST(创建新资源) 4.POST(批量操作) 5.PATCH(部分更新状态字段)