REST API 设计
概览
设计直观、一致且遵循行业最佳实践的资源导向架构的REST API。
何时使用
- 设计新的RESTful APIs
- 创建端点结构
- 定义请求/响应格式
- 实施API版本控制
- 文档化API规范
- 重构现有APIs
指令
1. 资源命名
✅ 好的资源名称(名词,复数)
GET /api/users
GET /api/users/123
GET /api/users/123/orders
POST /api/products
DELETE /api/products/456
❌ 坏的资源名称(动词,不一致)
GET /api/getUsers
POST /api/createProduct
GET /api/user/123 (不一致的单数/复数)
2. HTTP方法和操作
# CRUD操作
GET /api/users # 列出所有用户(读取集合)
GET /api/users/123 # 获取特定用户(读取单个)
POST /api/users # 创建新用户(创建)
PUT /api/users/123 # 完全替换用户(更新)
PATCH /api/users/123 # 部分更新用户(部分更新)
DELETE /api/users/123 # 删除用户(删除)
# 嵌套资源
GET /api/users/123/orders # 获取用户的订单
POST /api/users/123/orders # 为用户创建订单
GET /api/users/123/orders/456 # 获取特定订单
3. 请求示例
创建资源
POST /api/users
Content-Type: application/json
{
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"role": "admin"
}
响应:201 创建
位置:/api/users/789
{
"id": "789",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"role": "admin",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
更新资源
PATCH /api/users/789
Content-Type: application/json
{
"firstName": "Jonathan"
}
响应:200 OK
{
"id": "789",
"email": "john@example.com",
"firstName": "Jonathan",
"lastName": "Doe",
"role": "admin",
"updatedAt": "2025-01-15T11:00:00Z"
}
4. 查询参数
# 过滤
GET /api/products?category=electronics&inStock=true
# 排序
GET /api/users?sort=lastName,asc
# 分页
GET /api/users?page=2&limit=20
# 字段选择
GET /api/users?fields=id,email,firstName
# 搜索
GET /api/products?q=laptop
# 组合多个过滤器
GET /api/orders?status=pending&customer=123&sort=createdAt,desc&limit=50
5. 响应格式
成功响应
{
"data": {
"id": "123",
"email": "user@example.com",
"firstName": "John"
},
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"version": "1.0"
}
}
带分页的集合响应
{
"data": [
{ "id": "1", "name": "产品1" },
{ "id": "2", "name": "产品2" }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 145,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
},
"links": {
"self": "/api/products?page=2&limit=20",
"first": "/api/products?page=1&limit=20",
"prev": "/api/products?page=1&limit=20",
"next": "/api/products?page=3&limit=20",
"last": "/api/products?page=8&limit=20"
}
}
错误响应
{
"error": {
"code": "VALIDATION_ERROR",
"message": "输入数据无效",
"details": [
{
"field": "email",
"message": "电子邮件格式无效"
},
{
"field": "age",
"message": "必须至少18岁"
}
]
},
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"requestId": "abc-123-def"
}
}
6. HTTP状态码
成功:
200 OK - 成功的GET,PATCH,DELETE
201 Created - 成功的POST(资源创建)
204 No Content - 成功的DELETE(无响应体)
客户端错误:
400 Bad Request - 请求格式/数据无效
401 Unauthorized - 缺少或无效的身份验证
403 Forbidden - 已认证但无权限
404 Not Found - 资源不存在
409 Conflict - 资源冲突(例如,重复的电子邮件)
422 Unprocessable - 验证错误
429 Too Many Requests - 速率限制超出
服务器错误:
500 Internal Server Error - 通用服务器错误
503 Service Unavailable - 临时不可用
7. API版本控制
# URL路径版本控制(推荐)
GET /api/v1/users
GET /api/v2/users
# 头部版本控制
GET /api/users
Accept: application/vnd.myapi.v1+json
# 查询参数(不推荐)
GET /api/users?version=1
8. 认证与安全
# JWT Bearer Token
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# API密钥
GET /api/users
X-API-Key: your-api-key-here
# 生产中始终使用HTTPS
https://api.example.com/v1/users
9. 速率限制头部
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1642262400
10. OpenAPI文档
openapi: 3.0.0
info:
title: 用户API
version: 1.0.0
description: 用户管理API
paths:
/users:
get:
summary: 列出所有用户
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 成功响应
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: 创建新用户
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: 用户已创建
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: 输入无效
'409':
description: 电子邮件已存在
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
createdAt:
type: string
format: date-time
UserInput:
type: object
required:
- email
- firstName
- lastName
properties:
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
最佳实践
✅ 要做
- 使用名词作为资源,不要使用动词
- 对集合使用复数名称
- 保持命名约定的一致性
- 返回适当的HTTP状态码
- 为集合包含分页
- 提供过滤和排序选项
- 版本控制您的API
- 使用OpenAPI进行详细文档化
- 使用HTTPS
- 实施速率限制
- 提供清晰的错误消息
- 使用ISO 8601日期格式
❌ 不要做
- 在端点名称中使用动词
- 返回200错误
- 无必要地暴露内部ID
- 过度嵌套资源(最多2级)
- 使用不一致的命名
- 忘记认证
- 返回敏感数据
- 在没有版本控制的情况下破坏向后兼容性
完整示例:Express.js
const express = require('express');
const app = express();
app.use(express.json());
// 带分页列出用户
app.get('/api/v1/users', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const users = await User.findAndCountAll({
limit,
offset,
attributes: ['id', 'email', 'firstName', 'lastName']
});
res.json({
data: users.rows,
pagination: {
page,
limit,
total: users.count,
totalPages: Math.ceil(users.count / limit)
}
});
} catch (error) {
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: '获取用户时发生错误'
}
});
}
});
// 获取单个用户
app.get('/api/v1/users/:id', async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: 'NOT_FOUND',
message: '用户未找到'
}
});
}
res.json({ data: user });
} catch (error) {
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: '发生错误'
}
});
}
});
// 创建用户
app.post('/api/v1/users', async (req, res) => {
try {
const { email, firstName, lastName } = req.body;
// 验证
if (!email || !firstName || !lastName) {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: '缺少必填字段',
details: [
!email && { field: 'email', message: '电子邮件是必填项' },
!firstName && { field: 'firstName', message: '名字是必填项' },
!lastName && { field: 'lastName', message: '姓氏是必填项' }
].filter(Boolean)
}
});
}
const user = await User.create({ email, firstName, lastName });
res.status(201)
.location(`/api/v1/users/${user.id}`)
.json({ data: user });
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
error: {
code: 'CONFLICT',
message: '电子邮件已存在'
}
});
}
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: '发生错误'
}
});
}
});
app.listen(3000);