后端开发
核心原则
- 无状态服务 — 不要在内存中存储会话状态;使用 Redis 或数据库
- 快速失败 — 在边界处验证输入,尽早返回错误
- 幂等性 — 可安全多次调用的 POST/PUT 端点
- 可观测性 — 每个服务都需要日志、指标和追踪
API 端点结构
请求生命周期
请求 → 速率限制 → 认证 → 授权 → 验证 → 业务逻辑 → 响应
切勿跳过此顺序。先认证,后业务逻辑。先验证,后处理。
响应标准
// 成功
{ "data": { ... }, "meta": { "page": 1, "total": 100 } }
// 错误
{
"error": {
"code": "VALIDATION_ERROR",
"message": "人类可读的描述",
"details": [{ "field": "email", "issue": "格式无效" }]
}
}
认证模式
JWT 最佳实践
- 短访问令牌 TTL(15 分钟)+ 长刷新令牌(7 天)
- 将刷新令牌存储在 httpOnly cookie 中(而非 localStorage)
- 验证签名、过期时间、发行人和受众
- 使用后轮换刷新令牌
- 维护撤销列表或仅使用短寿命令牌
基于会话的认证
- 将会话 ID 存储在 httpOnly、Secure、SameSite cookie 中
- 将会话数据存储在 Redis 中,并设置 TTL
- 登录后重新生成会话 ID(防止会话固定)
授权
// 在服务层进行授权检查,而非仅在路由层
async function getOrder(userId: string, orderId: string) {
const order = await db.orders.findById(orderId)
if (!order) throw new NotFoundError()
// 授权检查 — 用户只能查看自己的订单
if (order.userId !== userId) throw new ForbiddenError()
return order
}
后台任务与队列
使用队列处理:
- 发送邮件
- 图片/文件处理
- 不需要同步的外部 API 调用
- 长时间运行的任务
任务模式
发射后不管:立即入队并返回
定时:在指定时间运行(cron)
延迟:在 X 分钟/小时后运行
重试:失败后以退避策略重试
始终实现:
- 死信队列处理失败任务
- 幂等性键防止重复处理
- 每个任务设置超时
- 最大重试次数
错误处理
// 结构化错误层次
class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public code: string
) { super(message) }
}
class ValidationError extends AppError {
constructor(details: ValidationDetail[]) {
super('验证失败', 400, 'VALIDATION_ERROR')
this.details = details
}
}
// 全局错误处理器
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err })
}
// 记录意外错误
logger.error('意外错误', { err, req })
res.status(500).json({ error: { code: 'INTERNAL_ERROR' } })
})
中间件清单
每个生产级 API 都需要:
- 请求日志(方法、路径、状态、耗时)
- 速率限制(按 IP、按用户、按端点)
- CORS 配置(明确的白名单)
- Helmet / 安全头
- 请求 ID 传播(用于分布式追踪)
- 请求体大小限制
- 超时处理
环境配置
// 切勿硬编码配置 — 始终从环境变量获取
const config = {
db: {
url: process.env.DATABASE_URL, // 必填
poolSize: parseInt(process.env.DB_POOL_SIZE ?? '10')
},
jwt: {
secret: process.env.JWT_SECRET, // 必填
expiresIn: process.env.JWT_TTL ?? '15m'
}
}
// 启动时验证必填配置 — 快速失败
const required = ['DATABASE_URL', 'JWT_SECRET']
for (const key of required) {
if (!process.env[key]) throw new Error(`缺少必填环境变量: ${key}`)
}