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 连接

返回技能库 更多技能入口