Node.js 后端实践
指导 Agent 在 Express/Fastify/Nest 等栈中组织路由、中间件与领域层,并统一错误、日志与配置注入。
本页为 Agent 提供 Node.js 后端实践的完整参考:Express 中间件链完整示例(认证/日志/限流/错误处理)、统一错误处理中间件、Zod 请求验证、环境变量类型安全解析,以及优雅关机(graceful shutdown)实现。
SKILL 定义中间件顺序、鉴权、Zod 校验失败的响应格式;禁止日志输出 password/token 等敏感字段;异步路由用 asyncHandler 包装,避免未捕获的 Promise 拒绝。
- 数据库:连接池、事务边界与 Prisma/Knex 迁移工具的引用方式。
- 可观测性:request id、结构化日志(pino)与监控技能对齐。
- 测试:supertest 集成测试 + mock 外部 HTTP(msw/nock)的约定。
请求流水线(中间件 → 路由)
[ TCP / TLS 终止(常由反向代理完成)]
│
▼
┌─────────────┐ 信任代理、request id、安全头、CORS、体大小/限流
│ 全局中间件 │──── 解析 cookie / body;勿在鉴权前信任未校验体
└─────────────┘
│
▼
┌─────────────┐ JWT / session / API key → 注入 user / tenant 上下文
│ 鉴权中间件 │──── 失败:统一 401/403 体,不泄漏内部原因细节
└─────────────┘
│
▼
┌─────────────┐ 路由参数 + DTO/schema 校验;失败:422/400 与字段级错误
│ 校验与路由 │──── 业务 handler 只接收已校验输入
└─────────────┘
│
▼
┌─────────────┐ 领域服务 / 仓储;抛出可映射的业务错误类型
│ 领域与 I/O │──── 未捕获:由外层 error middleware 映射为 5xx + 记录
└─────────────┘
│
▼
[ 响应序列化 ] ──► 404 / 全局错误处理(栈信息仅开发环境)
框架差异(Express 链式、Fastify 钩子、Nest 管道/守卫/拦截器)名称不同,但「越靠近入口越通用、鉴权在解析体之后与业务之前、集中错误在最后」的原则一致。
Express 中间件链完整示例与 Zod 验证
Express 中间件完整链(带注释说明顺序):
// app.ts — Express 中间件完整配置
import express from 'express'
import helmet from 'helmet'
import cors from 'cors'
import rateLimit from 'express-rate-limit'
import pinoHttp from 'pino-http'
import { authenticate } from './middleware/auth'
import { errorHandler } from './middleware/errorHandler'
import { itemsRouter } from './routes/items'
const app = express()
// 1. 信任代理(反向代理后必须配置)
app.set('trust proxy', 1)
// 2. 安全头(尽早设置)
app.use(helmet())
// 3. CORS(预检在 body 解析前完成)
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
// 4. 请求日志 + Request ID 注入
app.use(pinoHttp({
genReqId: (req) => req.headers['x-request-id'] ?? crypto.randomUUID(),
redact: ['req.headers.authorization', 'req.body.password'],
}))
// 5. 基础限流(在 body 解析前)
app.use(rateLimit({ windowMs: 60_000, max: 100, standardHeaders: true }))
// 6. Body 解析(限制大小)
app.use(express.json({ limit: '1mb' }))
app.use(express.urlencoded({ extended: true, limit: '1mb' }))
// 7. 鉴权(body 解析后,路由前)
app.use('/api', authenticate)
// 8. 业务路由
app.use('/api/v1/items', itemsRouter)
// 9. 404 处理
app.use((_req, res) => res.status(404).json({ type: 'not-found', status: 404 }))
// 10. 全局错误处理(必须放最后,4个参数)
app.use(errorHandler)
Zod 请求验证中间件:
// middleware/validate.ts
import { z, ZodSchema } from 'zod'
import { Request, Response, NextFunction } from 'express'
export function validate<T>(schema: ZodSchema<T>, target: 'body' | 'query' | 'params' = 'body') {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req[target])
if (!result.success) {
return res.status(422).json({
type: 'https://api.example.com/problems/validation-error',
title: 'Validation Error',
status: 422,
errors: result.error.issues.map(issue => ({
field: issue.path.join('.'),
code: issue.code,
message: issue.message,
})),
})
}
req[target] = result.data // 替换为已验证数据
next()
}
}
// 使用示例
const CreateItemSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
sku: z.string().regex(/^[A-Z]{3}-\d{3}$/),
})
router.post('/', validate(CreateItemSchema), async (req, res, next) => {
// req.body 已经是 CreateItem 类型,且通过了 Zod 校验
})
统一错误处理、环境变量与优雅关机
统一错误处理中间件:
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express'
export class AppError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
message: string,
public readonly isOperational = true
) { super(message) }
}
// asyncHandler 包装器(避免在每个路由写 try/catch)
export const asyncHandler = (fn: Function) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next)
// 集中错误处理(必须 4 个参数)
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
const isProd = process.env.NODE_ENV === 'production'
if (err instanceof AppError && err.isOperational) {
// 已知业务错误 → 4xx
return res.status(err.status).json({
type: `https://api.example.com/problems/${err.code}`,
title: err.message,
status: err.status,
requestId: (req as any).id,
})
}
// 未知错误 → 5xx(生产环境隐藏细节)
req.log?.error({ err, reqId: (req as any).id }, 'Unhandled error')
res.status(500).json({
type: 'https://api.example.com/problems/internal-error',
title: 'Internal Server Error',
status: 500,
...(isProd ? {} : { detail: err.message, stack: err.stack }),
})
}
类型安全的环境变量解析(Zod):
// config/env.ts
import { z } from 'zod'
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional(),
ALLOWED_ORIGINS: z.string().optional(),
})
const parsed = EnvSchema.safeParse(process.env)
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.format())
process.exit(1)
}
export const env = parsed.data
优雅关机(Graceful Shutdown):
// server.ts
const server = app.listen(env.PORT, () => {
console.log(`Server listening on port ${env.PORT}`)
})
async function gracefulShutdown(signal: string) {
console.log(`${signal} received. Starting graceful shutdown...`)
// 1. 停止接受新连接
server.close(async (err) => {
if (err) console.error('Error closing server:', err)
// 2. 关闭数据库连接池
await db.$disconnect()
// 3. 关闭 Redis 连接
await redis.quit()
console.log('Graceful shutdown complete')
process.exit(err ? 1 : 0)
})
// 4. 超时强制退出(避免卡死)
setTimeout(() => {
console.error('Shutdown timeout, forcing exit')
process.exit(1)
}, 30_000)
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
与「可观测性」技能对齐:同一 request id 应贯穿 access log、业务日志与错误上报,便于从 4xx/5xx 反查链路。
中间件顺序笔记生成器
勾选本服务实际用到的能力,生成按推荐顺序编号的片段,可直接粘贴进 SKILL 的「中间件顺序」小节;顺序按常见 Express/Fastify 实践排列,若你栈有特殊要求请再手工调整。
未勾选任何项时,输出占位说明;全选时得到完整参考链。Nest 用户可将条目映射为 Middleware → Guard → Pipe → Interceptor → Filter。
---
name: node-backend-practices
description: 按分层与错误模型实现 Node HTTP 服务
---
# 规则
- 中间件顺序:trust proxy → helmet → CORS → 日志+reqId → 限流 → body解析 → 鉴权 → 路由 → 404 → errorHandler
- 异步路由必须用 asyncHandler 包装,或 async/await + try/catch + next(err)
- 错误分层:AppError(业务/4xx)vs 未知错误(5xx),统一在 errorHandler 映射
- 请求验证:Zod safeParse,422 响应带 errors 数组(field/code/message)
- 环境变量:Zod EnvSchema 启动时校验,缺失/格式错误则 process.exit(1)
- 日志:pino 结构化,redact password/token/Authorization
- 优雅关机:SIGTERM → server.close() → db.$disconnect() → 30s 超时强制退出
# 步骤
1. 创建 config/env.ts:Zod 解析 process.env,导出 env 对象
2. 创建 middleware/errorHandler.ts:AppError 类 + asyncHandler + errorHandler
3. 创建 app.ts:按顺序挂载中间件链
4. 在路由中用 validate(Schema) 中间件校验 body/query/params
5. 业务逻辑 throw new AppError(status, code, message)
6. 在 server.ts 监听 SIGTERM/SIGINT,实现 gracefulShutdown
7. 集成测试:supertest(app) + beforeAll/afterAll 管理 DB 连接