构建设计良好的 REST API 是现代开发者最重要的技能之一。优秀的 API 直观、一致、安全且易于维护。本综合指南涵盖从 URL 命名约定和 HTTP 方法到认证、分页、限流和常见错误的所有内容。无论你是在构建第一个 API 还是在优化现有 API,这些最佳实践都将帮助你创建开发者喜爱的 API。
URL 命名约定
良好 REST API 的基础从精心设计的 URL 开始。你的端点应该是可预测的、可读的,并遵循一致的模式。
使用名词,而非动词
REST 资源应该用名词来标识。HTTP 方法已经指示了操作,因此不需要在 URL 路径中包含动词。将 URL 视为资源的地址,而不是命令。
# Good - nouns as resources
GET /users # Get all users
GET /users/123 # Get user 123
POST /users # Create a new user
DELETE /users/123 # Delete user 123
# Bad - verbs in URLs
GET /getUsers
POST /createUser
DELETE /deleteUser/123集合使用复数形式
始终对集合端点使用复数名词。无论是引用集合还是集合中的单个资源,这都保持了一致性。
# Good - consistent plurals
GET /users # Collection
GET /users/123 # Single resource in collection
GET /products # Collection
GET /products/456 # Single resource
# Bad - inconsistent or singular
GET /user # Ambiguous: one user or all users?
GET /user/123
GET /product-list嵌套资源表示关系
使用嵌套来表达资源之间的关系,但避免超过两层。深层嵌套使 URL 难以阅读和维护。
# Good - clear parent-child relationship (max 2 levels)
GET /users/123/orders # Orders belonging to user 123
GET /users/123/orders/456 # Specific order of user 123
# Bad - too deeply nested
GET /users/123/orders/456/items/789/reviews
# Better: use a direct resource endpoint
GET /order-items/789/reviews
# Or: flatten with query parameters
GET /reviews?order_item_id=789URL 模式:好 vs 坏
| 好 | 坏 | 原因 |
|---|---|---|
| GET /users | GET /getUsers | HTTP method already implies the action |
| GET /users/123 | GET /user?id=123 | Use path parameters for resource identity |
| POST /users | POST /createUser | POST already means create |
| PUT /users/123 | POST /updateUser | PUT/PATCH for updates, use resource path |
| DELETE /users/123 | POST /deleteUser | Use DELETE method, not POST with verb |
| GET /users/123/orders | GET /getUserOrders?userId=123 | Use nested resources for relationships |
| GET /products?category=electronics | GET /electronics/products | Use query params for filtering |
| GET /users?status=active&sort=name | GET /active-users-sorted-by-name | Use query params, not encoded URLs |
| PATCH /users/123 | POST /users/123/update | Use HTTP methods, not URL verbs |
| GET /users/123/avatar | GET /getAvatarForUser/123 | Keep it resource-oriented |
HTTP 方法
每个 HTTP 方法都有特定的语义。正确使用它们使你的 API 可预测,并允许客户端无需阅读文档即可理解每个请求的意图。
| 方法 | 用途 | 幂等性 | 请求体 | 示例 |
|---|---|---|---|---|
| GET | Retrieve a resource or collection | Yes | No | GET /users/123 |
| POST | Create a new resource | No | Yes | POST /users |
| PUT | Replace a resource entirely | Yes | Yes | PUT /users/123 |
| PATCH | Partially update a resource | No* | Yes | PATCH /users/123 |
| DELETE | Remove a resource | Yes | Optional | DELETE /users/123 |
* PATCH can be idempotent depending on implementation. If the patch describes the final state (e.g., {"name": "John"}), it is idempotent. If it describes an operation (e.g., {"op": "increment", "path": "/count"}), it is not.
HTTP 状态码
使用正确的状态码帮助客户端适当地处理响应。以下是 REST API 最重要的状态码及其使用时机。
| 状态码 | 名称 | 何时使用 |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, or DELETE. Return the resource or confirmation. |
| 201 | Created | Successful POST that created a new resource. Include Location header with the new resource URL. |
| 204 | No Content | Successful DELETE or PUT/PATCH when no response body is needed. |
| 400 | Bad Request | The request is malformed — invalid JSON, missing required fields, wrong data types. |
| 401 | Unauthorized | No valid authentication credentials provided. The client must authenticate first. |
| 403 | Forbidden | The client is authenticated but does not have permission for this action. |
| 404 | Not Found | The requested resource does not exist. Also use for hidden resources (instead of 403) for security. |
| 409 | Conflict | The request conflicts with the current state — duplicate email, version mismatch, etc. |
| 422 | Unprocessable Entity | The request is well-formed but fails validation — email format invalid, password too short. |
| 429 | Too Many Requests | Rate limit exceeded. Include Retry-After header with seconds until the client can retry. |
| 500 | Internal Server Error | An unexpected server error occurred. Log the details server-side but do not expose them to clients. |
| 503 | Service Unavailable | The server is temporarily unavailable — maintenance mode, overloaded. Include Retry-After header. |
错误处理
一致的错误响应使你的 API 更容易使用。每个错误都应该返回结构化的 JSON 正文,包含足够的信息让客户端理解和修复问题。
标准错误响应格式
对所有错误响应使用一致的错误封装。包括机器可读的错误代码、人类可读的消息和可选的详细信息。
// Standard error response
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The user with ID 123 was not found.",
"status": 404,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_abc123def456",
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
}
}
// Another example: authentication error
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "The access token has expired. Please refresh your token.",
"status": 401,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_xyz789"
}
}带字段级详情的验证错误
对于 422 验证错误,包含字段级错误信息,以便客户端在正确的表单字段旁显示错误。
// 422 Validation error with field-level details
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid fields.",
"status": 422,
"details": [
{
"field": "email",
"message": "Must be a valid email address.",
"rejected_value": "not-an-email"
},
{
"field": "password",
"message": "Must be at least 8 characters long.",
"rejected_value": "short"
},
{
"field": "age",
"message": "Must be a positive integer.",
"rejected_value": -5
}
],
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_val456"
}
}认证与授权
选择正确的认证策略取决于你的使用场景。以下是三种最常见方法的比较。
API 密钥
通过头部或查询参数传递的简单字符串令牌。最适合服务器间通信和简单集成。易于实现但安全性有限,因为密钥是长期有效的且难以限定范围。
# API Key in header (recommended)
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456ghi789
# API Key in query parameter (less secure - visible in logs)
GET /api/users?api_key=sk_live_abc123def456ghi789JWT Bearer 令牌
携带用户声明的自包含令牌。服务器可以在不查询数据库的情况下验证令牌。非常适合微服务中的无状态认证。令牌应该是短期的,配合刷新令牌轮换。
# JWT Bearer Token in Authorization header
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
# Token refresh flow
POST /api/auth/refresh HTTP/1.1
Content-Type: application/json
{
"refresh_token": "rt_abc123def456"
}
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_new789xyz"
}OAuth 2.0
委托授权的行业标准。允许第三方应用代表用户访问资源而不暴露凭证。实现更复杂但提供最大的灵活性和安全性。
# OAuth 2.0 Authorization Code Flow
# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
response_type=code&
client_id=your_client_id&
redirect_uri=https://yourapp.com/callback&
scope=read:users write:users&
state=random_csrf_token
# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback
# Step 3: Use the access token
GET /api/users/me HTTP/1.1
Authorization: Bearer ACCESS_TOKEN_HERE认证方式比较
| 特性 | API Key | JWT | OAuth 2.0 |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Stateless | Yes | Yes | Depends |
| User context | No | Yes | Yes |
| Token expiration | Manual rotation | Built-in (exp claim) | Built-in |
| Scope/permissions | Limited | Via claims | Fine-grained scopes |
| Third-party access | No | No | Yes (delegated) |
| Best for | Server-to-server | SPAs, Mobile apps | Third-party integrations |
| Revocation | Delete key | Blacklist needed | Revoke at auth server |
分页模式
任何返回资源列表的端点都应该支持分页。没有分页,随着数据增长,响应可能变得巨大且缓慢。
基于偏移的分页
最简单的方法。使用页码和限制参数。易于实现并允许跳转到任何页面,但当数据在请求之间被修改时可能不一致。
# Offset-based pagination request
GET /api/users?page=2&limit=20
# How it works internally
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20; -- Skip first 20, return next 20
# Pros: Simple, allows jumping to any page
# Cons: Inconsistent with concurrent modifications,
# slow on large offsets (OFFSET 100000)基于游标的分页
使用不透明的游标令牌来标记数据集中的位置。对于实时数据和大型数据集更可靠,因为当数据变化时不会跳过或重复项目。
# Cursor-based pagination request
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
# The cursor is an opaque token (often base64-encoded)
# that points to a specific position in the dataset
# How it works internally (cursor decodes to {"id": 123})
SELECT * FROM users
WHERE id > 123
ORDER BY id ASC
LIMIT 20;
# Pros: Consistent results, fast on large datasets
# Cons: Cannot jump to arbitrary pages,
# only next/previous navigation分页响应格式
在每个分页响应中包含分页状态的元数据。这允许客户端构建导航控件。
// Offset-based pagination response
{
"data": [
{ "id": 21, "name": "Alice" },
{ "id": 22, "name": "Bob" }
// ... 18 more items
],
"meta": {
"current_page": 2,
"per_page": 20,
"total_pages": 15,
"total_count": 294
},
"links": {
"self": "/api/users?page=2&limit=20",
"first": "/api/users?page=1&limit=20",
"prev": "/api/users?page=1&limit=20",
"next": "/api/users?page=3&limit=20",
"last": "/api/users?page=15&limit=20"
}
}
// Cursor-based pagination response
{
"data": [
{ "id": 124, "name": "Charlie" },
{ "id": 125, "name": "Diana" }
// ... 18 more items
],
"meta": {
"has_more": true,
"next_cursor": "eyJpZCI6MTQzfQ",
"prev_cursor": "eyJpZCI6MTI0fQ"
},
"links": {
"next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
"prev": "/api/users?cursor=eyJpZCI6MTI0fQ&limit=20&direction=prev"
}
}API 版本控制策略
API 随时间演进。版本控制允许你在不中断现有客户端的情况下进行破坏性更改。以下是三种最常见的方法。
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL Path | /v1/users, /v2/users | Explicit, easy to route and cache, clear in logs and docs | URL pollution, harder to sunset old versions |
| Custom Header | Accept-Version: v2 or X-API-Version: 2 | Clean URLs, version hidden from casual users | Harder to test (need tools to set headers), not cacheable by default |
| Query Parameter | /users?version=2 | Easy to test in browser, no URL path changes | Can be accidentally omitted, harder to route, pollutes query string |
| Content Negotiation | Accept: application/vnd.api.v2+json | Most RESTful approach, per-resource versioning | Complex, hard to test, poorly supported by many tools |
# URL Path versioning (recommended)
GET /v1/users/123 # Version 1
GET /v2/users/123 # Version 2 with breaking changes
# Express.js router example
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);
app.use('/v1', v1Router);
app.use('/v2', v2Router);限流
限流保护你的 API 免受滥用并确保所有客户端的公平使用。始终通过响应头传达限流状态。
限流头部
在每个响应中包含这些头部,以便客户端监控其使用情况并避免达到限制。
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 847 # Remaining requests in window
X-RateLimit-Reset: 1706176800 # Unix timestamp when window resets
X-RateLimit-Window: 3600 # Window duration in seconds (1 hour)
# Some APIs also include:
RateLimit-Policy: 1000;w=3600 # IETF draft standard format429 请求过多响应
当客户端超过限流时,返回带有 Retry-After 头的 429 状态,指示他们何时可以再次发出请求。
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706176800
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 1000 requests per hour.",
"status": 429,
"retry_after": 45,
"documentation_url": "https://api.example.com/docs/rate-limiting"
}
}退避策略
客户端在收到 429 响应时应实现指数退避。从短延迟开始,每次重试加倍,直到最大值。
// Exponential backoff with jitter
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Use Retry-After header if available
const retryAfter = response.headers.get('Retry-After');
let delay;
if (retryAfter) {
delay = parseInt(retryAfter, 10) * 1000;
} else {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const baseDelay = Math.pow(2, attempt) * 1000;
// Add jitter (random 0-1s) to prevent thundering herd
const jitter = Math.random() * 1000;
delay = baseDelay + jitter;
}
console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}安全头部
安全头部保护你的 API 免受常见的 Web 漏洞。这些头部应包含在 API 的每个响应中。
| 头部 | 推荐值 | 用途 |
|---|---|---|
| Access-Control-Allow-Origin | https://yourapp.com (specific origin) | CORS: Controls which domains can call your API. Never use * in production with credentials. |
| Access-Control-Allow-Methods | GET, POST, PUT, PATCH, DELETE | CORS: Specifies which HTTP methods are allowed for cross-origin requests. |
| Access-Control-Allow-Headers | Authorization, Content-Type | CORS: Specifies which request headers are allowed in cross-origin requests. |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | HSTS: Forces browsers to use HTTPS for all future requests to your domain. |
| Content-Security-Policy | default-src 'none'; frame-ancestors 'none' | CSP: Prevents XSS and data injection attacks. For APIs, restrict everything. |
| X-Content-Type-Options | nosniff | Prevents browsers from MIME-type sniffing. Ensures responses are treated as their declared content type. |
| X-Frame-Options | DENY | Prevents your API responses from being embedded in iframes (clickjacking protection). |
| Cache-Control | no-store, no-cache, must-revalidate | Prevents caching of sensitive API responses. Adjust per endpoint as needed. |
// Express.js security headers middleware
const helmet = require('helmet');
app.use(helmet());
// Custom CORS configuration
const cors = require('cors');
app.use(cors({
origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', 'no-store');
next();
});使用 OpenAPI 编写 API 文档
良好的文档对 API 的采用至关重要。OpenAPI(以前称为 Swagger)是描述 REST API 的标准。它支持自动生成文档、客户端 SDK 和测试工具。
- 先编写 OpenAPI 规范(设计优先方法),然后实现 API 以匹配规范。
- 为每个端点、参数和响应 schema 包含详细描述。
- 为所有请求和响应正文提供真实的示例值。
- 为每个端点记录错误响应,不仅仅是成功情况。
- 使用标签对相关端点进行分组,使文档易于导航。
- 将 OpenAPI 规范与 API 代码一起进行版本控制。
- 设置交互式文档(Swagger UI 或 Redoc),以便开发者可以直接尝试请求。
# OpenAPI 3.1 specification example
openapi: 3.1.0
info:
title: User Management API
version: 1.0.0
description: API for managing users and their resources
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: A paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
example:
data:
- id: 1
name: "Alice Johnson"
email: "alice@example.com"
meta:
current_page: 1
total_pages: 10
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a new user
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
example:
name: "Bob Smith"
email: "bob@example.com"
password: "securePassword123"
responses:
'201':
description: User created successfully
'422':
$ref: '#/components/responses/ValidationError'常见 REST API 错误
即使是经验丰富的开发者也会犯这些错误。查看此列表以确保你的 API 遵循最佳实践。
| # | 错误 | 为什么是错的 | 修复方法 |
|---|---|---|---|
| 1 | Using verbs in URLs | REST is resource-oriented; HTTP methods express actions | Use nouns: /users not /getUsers |
| 2 | Returning 200 for errors | Clients rely on status codes for error handling | Use proper status codes: 400, 404, 422, 500 |
| 3 | Not paginating list endpoints | Returns grow unbounded, causing timeouts and memory issues | Always paginate with sensible defaults (limit=20) |
| 4 | Exposing internal errors | Stack traces reveal implementation details to attackers | Log details server-side, return generic error messages |
| 5 | Ignoring CORS headers | Browser-based clients cannot call your API | Configure CORS for allowed origins and methods |
| 6 | Not versioning the API | Breaking changes affect all clients simultaneously | Use URL path versioning: /v1/users |
| 7 | Using POST for everything | Loses HTTP method semantics, breaks caching and idempotency | Use GET, POST, PUT, PATCH, DELETE appropriately |
| 8 | Inconsistent naming conventions | Mixed case or formats confuse API consumers | Pick one (snake_case or camelCase) and be consistent |
| 9 | No rate limiting | API is vulnerable to abuse and DDoS attacks | Implement rate limiting with clear headers |
| 10 | Missing Content-Type header | Clients cannot parse responses correctly | Always set Content-Type: application/json |
常见问题
REST 和 RESTful 有什么区别?
REST(表述性状态转移)是 Roy Fielding 定义的架构风格。RESTful 是描述遵守 REST 约束的 API 的形容词。实际上,大多数"REST API"并未严格遵循所有 REST 约束(如 HATEOAS),但这两个术语可以互换使用。
更新资源应该使用 PUT 还是 PATCH?
当客户端发送资源的完整替换时使用 PUT。对于只发送更改字段的部分更新使用 PATCH。PATCH 对大型资源更节省带宽。大多数现代 API 倾向于对典型更新操作使用 PATCH。
应该如何处理 API 版本控制?
URL 路径版本控制(例如 /v1/users)是最常见且最明确的方法。它易于理解、路由和缓存。头部版本控制更简洁但更难测试和调试。选择一种策略并在整个 API 中一致应用。
在 REST API 中处理认证的最佳方式是什么?
对于大多数应用,JWT Bearer 令牌提供了安全性和简单性的良好平衡。使用短期访问令牌(15-60 分钟)配合刷新令牌轮换。对于第三方集成,使用 OAuth 2.0。API 密钥仅适用于服务器间通信。
如何在 REST API 中设计嵌套资源?
使用嵌套显示父子关系:/users/123/orders。最多限制两层嵌套。对于更深的关系,使用查询参数或提供直接资源端点。例如,用 /order-items/789 或 /orders/456/items/789 代替 /users/123/orders/456/items/789。
REST API 响应应该包含 null 字段还是省略它们?
这取决于你的约定。包含 null 字段使 schema 可预测——客户端总是知道期望什么字段。省略 null 字段减少负载大小。保持一致:选择一种方法并记录下来。