Loading...
Loading...
Compare original and translation side by side
undefinedundefinedundefinedundefined// ❌ BAD: Verb-based endpoints
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
GET /getUserOrders/123
// ✅ GOOD: Resource-based endpoints
GET /users # List users
POST /users # Create user
GET /users/123 # Get user
PUT /users/123 # Update user (full)
PATCH /users/123 # Update user (partial)
DELETE /users/123 # Delete user
GET /users/123/orders # User's orders (nested resource)
// ✅ GOOD: Filtering, sorting, pagination
GET /users?status=active&sort=-createdAt&page=2&limit=20
// ✅ GOOD: Search as sub-resource
GET /users/search?q=john&fields=name,email
// ✅ GOOD: Actions as sub-resources (when needed)
POST /users/123/activate # Action on resource
POST /orders/456/cancel # State transition// ❌ 不良示例:基于动词的端点
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
GET /getUserOrders/123
// ✅ 良好示例:基于资源的端点
GET /users # 列出用户
POST /users # 创建用户
GET /users/123 # 获取单个用户
PUT /users/123 # 完整更新用户
PATCH /users/123 # 部分更新用户
DELETE /users/123 # 删除用户
GET /users/123/orders # 用户的订单(嵌套资源)
// ✅ 良好示例:过滤、排序、分页
GET /users?status=active&sort=-createdAt&page=2&limit=20
// ✅ 良好示例:搜索作为子资源
GET /users/search?q=john&fields=name,email
// ✅ 良好示例:操作作为子资源(必要时使用)
POST /users/123/activate # 对资源执行激活操作
POST /orders/456/cancel # 状态转换// Method characteristics
// GET - Safe, Idempotent, Cacheable
// POST - Not Safe, Not Idempotent
// PUT - Not Safe, Idempotent
// PATCH - Not Safe, Not Idempotent
// DELETE - Not Safe, Idempotent
// Express example with proper methods
import { Router } from 'express';
const router = Router();
// GET - Retrieve resources (safe, idempotent)
router.get('/products', listProducts);
router.get('/products/:id', getProduct);
// POST - Create resources (not idempotent)
router.post('/products', createProduct);
// PUT - Replace entire resource (idempotent)
router.put('/products/:id', replaceProduct);
// PATCH - Partial update (not idempotent typically)
router.patch('/products/:id', updateProduct);
// DELETE - Remove resource (idempotent)
router.delete('/products/:id', deleteProduct);// 2xx Success
200 OK // GET success, PUT/PATCH success with body
201 Created // POST success (include Location header)
204 No Content // DELETE success, PUT/PATCH success without body
// 3xx Redirection
301 Moved Permanently // Resource URL changed permanently
304 Not Modified // Cached response is still valid
// 4xx Client Errors
400 Bad Request // Invalid request body/params
401 Unauthorized // Missing or invalid authentication
403 Forbidden // Authenticated but not authorized
404 Not Found // Resource doesn't exist
405 Method Not Allowed // HTTP method not supported
409 Conflict // State conflict (e.g., duplicate)
422 Unprocessable Entity // Validation errors
429 Too Many Requests // Rate limit exceeded
// 5xx Server Errors
500 Internal Server Error // Unexpected server error
502 Bad Gateway // Upstream service error
503 Service Unavailable // Temporary overload/maintenance// 方法特性
// GET - 安全、幂等、可缓存
// POST - 不安全、非幂等
// PUT - 不安全、幂等
// PATCH - 不安全、非幂等
// DELETE - 不安全、幂等
// 符合规范的Express示例
import { Router } from 'express';
const router = Router();
// GET - 获取资源(安全、幂等)
router.get('/products', listProducts);
router.get('/products/:id', getProduct);
// POST - 创建资源(非幂等)
router.post('/products', createProduct);
// PUT - 替换整个资源(幂等)
router.put('/products/:id', replaceProduct);
// PATCH - 部分更新(通常非幂等)
router.patch('/products/:id', updateProduct);
// DELETE - 删除资源(幂等)
router.delete('/products/:id', deleteProduct);// 2xx 成功
200 OK // GET请求成功,PUT/PATCH请求成功并返回响应体
201 Created // POST请求成功(需包含Location响应头)
204 No Content // DELETE请求成功,PUT/PATCH请求成功但无响应体
// 3xx 重定向
301 Moved Permanently // 资源URL永久变更
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 // 服务临时过载或维护中// Error response structure
interface ApiError {
status: number; // HTTP status code
code: string; // Application-specific error code
message: string; // Human-readable message
details?: ErrorDetail[]; // Field-level errors (for validation)
requestId?: string; // For debugging/support
timestamp?: string; // ISO 8601
}
interface ErrorDetail {
field: string;
message: string;
code: string;
}
// Example responses
// 400 Bad Request - Validation Error
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Invalid email format", "code": "INVALID_EMAIL" },
{ "field": "age", "message": "Must be at least 18", "code": "MIN_VALUE" }
],
"requestId": "req_abc123",
"timestamp": "2024-01-15T10:30:00Z"
}
// 404 Not Found
{
"status": 404,
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID '123' not found",
"requestId": "req_def456",
"timestamp": "2024-01-15T10:30:00Z"
}
// 500 Internal Server Error
{
"status": 500,
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later.",
"requestId": "req_ghi789",
"timestamp": "2024-01-15T10:30:00Z"
}// Express error handler
import { Request, Response, NextFunction } from 'express';
class AppError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: ErrorDetail[]
) {
super(message);
}
}
function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
const requestId = req.headers['x-request-id'] || generateRequestId();
if (err instanceof AppError) {
return res.status(err.status).json({
status: err.status,
code: err.code,
message: err.message,
details: err.details,
requestId,
timestamp: new Date().toISOString(),
});
}
// Log unexpected errors
console.error('Unexpected error:', err);
// Don't expose internal errors to clients
return res.status(500).json({
status: 500,
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId,
timestamp: new Date().toISOString(),
});
}// 错误响应结构
interface ApiError {
status: number; // HTTP状态码
code: string; // 应用特定错误码
message: string; // 人类可读的错误信息
details?: ErrorDetail[]; // 字段级错误(用于验证场景)
requestId?: string; // 用于调试/支持
timestamp?: string; // ISO 8601格式时间
}
interface ErrorDetail {
field: string;
message: string;
code: string;
}
// 响应示例
// 400 Bad Request - 验证错误
{
"status": 400,
"code": "VALIDATION_ERROR",
"message": "请求验证失败",
"details": [
{ "field": "email", "message": "Invalid email format", "code": "INVALID_EMAIL" },
{ "field": "age", "message": "Must be at least 18", "code": "MIN_VALUE" }
],
"requestId": "req_abc123",
"timestamp": "2024-01-15T10:30:00Z"
}
// 404 Not Found
{
"status": 404,
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID '123' not found",
"requestId": "req_def456",
"timestamp": "2024-01-15T10:30:00Z"
}
// 500 Internal Server Error
{
"status": 500,
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later.",
"requestId": "req_ghi789",
"timestamp": "2024-01-15T10:30:00Z"
}// Express错误处理器
import { Request, Response, NextFunction } from 'express';
class AppError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: ErrorDetail[]
) {
super(message);
}
}
function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
const requestId = req.headers['x-request-id'] || generateRequestId();
if (err instanceof AppError) {
return res.status(err.status).json({
status: err.status,
code: err.code,
message: err.message,
details: err.details,
requestId,
timestamp: new Date().toISOString(),
});
}
// 记录意外错误
console.error('Unexpected error:', err);
// 不要向客户端暴露内部错误
return res.status(500).json({
status: 500,
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId,
timestamp: new Date().toISOString(),
});
}// 1. Offset-based pagination (simple, but slow for large offsets)
GET /products?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
// 2. Cursor-based pagination (efficient for large datasets)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODB9",
"hasNext": true,
"hasPrev": true
}
}
// Implementation example
async function paginateWithCursor(
cursor: string | null,
limit: number = 20
) {
const decodedCursor = cursor
? JSON.parse(Buffer.from(cursor, 'base64').toString())
: null;
const items = await prisma.product.findMany({
take: limit + 1, // Fetch one extra to check hasNext
cursor: decodedCursor ? { id: decodedCursor.id } : undefined,
skip: decodedCursor ? 1 : 0,
orderBy: { id: 'asc' },
});
const hasNext = items.length > limit;
const data = hasNext ? items.slice(0, -1) : items;
return {
data,
pagination: {
limit,
nextCursor: hasNext
? Buffer.from(JSON.stringify({ id: data[data.length - 1].id })).toString('base64')
: null,
hasNext,
},
};
}// 1. 基于偏移量的分页(简单,但大偏移量时性能慢)
GET /products?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
// 2. 基于游标的分页(大数据集下效率高)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"pagination": {
"limit": 20,
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODB9",
"hasNext": true,
"hasPrev": true
}
}
// 实现示例
async function paginateWithCursor(
cursor: string | null,
limit: number = 20
) {
const decodedCursor = cursor
? JSON.parse(Buffer.from(cursor, 'base64').toString())
: null;
const items = await prisma.product.findMany({
take: limit + 1, // 多获取一条数据用于判断是否有下一页
cursor: decodedCursor ? { id: decodedCursor.id } : undefined,
skip: decodedCursor ? 1 : 0,
orderBy: { id: 'asc' },
});
const hasNext = items.length > limit;
const data = hasNext ? items.slice(0, -1) : items;
return {
data,
pagination: {
limit,
nextCursor: hasNext
? Buffer.from(JSON.stringify({ id: data[data.length - 1].id })).toString('base64')
: null,
hasNext,
},
};
}// 1. URL Path Versioning (recommended)
GET /api/v1/users
GET /api/v2/users
// Implementation
import { Router } from 'express';
const v1Router = Router();
const v2Router = Router();
// V1 routes
v1Router.get('/users', getUsersV1);
// V2 routes with breaking changes
v2Router.get('/users', getUsersV2);
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// 2. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
// 3. Query Parameter (not recommended for APIs)
GET /api/users?version=2// Communicate deprecation
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT');
res.setHeader('Link', '</api/v2/users>; rel="successor-version"');// 1. URL路径版本控制(推荐)
GET /api/v1/users
GET /api/v2/users
// 实现示例
import { Router } from 'express';
const v1Router = Router();
const v2Router = Router();
// V1版本路由
v1Router.get('/users', getUsersV1);
// V2版本路由(包含破坏性变更)
v2Router.get('/users', getUsersV2);
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// 2. 请求头版本控制
GET /api/users
Accept: application/vnd.myapi.v2+json
// 3. 查询参数版本控制(不推荐用于API)
GET /api/users?version=2// 传达废弃信息
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Sat, 01 Jun 2025 00:00:00 GMT');
res.setHeader('Link', '</api/v2/users>; rel="successor-version"');// Consistent naming convention (pick one, stick to it)
// JavaScript/TypeScript typically uses camelCase
// Request validation with Zod
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().min(18).optional(),
role: z.enum(['user', 'admin']).default('user'),
});
// Validate in middleware
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
status: 400,
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
code: e.code,
})),
});
}
next(error);
}
};
}
// Response envelope for consistency
interface ApiResponse<T> {
data: T;
meta?: {
pagination?: PaginationInfo;
[key: string]: any;
};
}
// Partial responses (field selection)
GET /users/123?fields=id,name,email// 统一命名约定(选定一种并保持一致)
// JavaScript/TypeScript通常使用camelCase
// 使用Zod进行请求验证
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().min(18).optional(),
role: z.enum(['user', 'admin']).default('user'),
});
// 中间件中实现验证
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
status: 400,
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
code: e.code,
})),
});
}
next(error);
}
};
}
// 统一响应包装格式
interface ApiResponse<T> {
data: T;
meta?: {
pagination?: PaginationInfo;
[key: string]: any;
};
}
// 部分响应(字段选择)
GET /users/123?fields=id,name,email/createUser/updateProduct/createUser/updateProduct