Skip to content

Node.js Web 框架

目录

  1. Express 框架
  2. Koa 框架
  3. Nest.js 框架
  4. 框架选型对比
  5. 中间件进阶
  6. 路由设计最佳实践
  7. 面试高频问题
  8. 延伸阅读

1. Express 框架

Express 是 Node.js 生态中最早也是最主流的 Web 框架,自 2010 年发布以来,奠定了"中间件"这一核心架构范式。它的设计哲学是极简——框架本身只提供路由和中间件调度,其余功能全部通过插拔中间件实现。

1.1 中间件机制原理(线性模型)

Express 的中间件本质是一个函数签名为 (req, res, next) 的函数。所有中间件按注册顺序排列在一个栈中,通过调用 next() 将控制权单向传递给下一个中间件。这就是所谓的线性执行模型——请求从第一个中间件流向最后一个,没有"回溯"阶段。

请求流转路径(线性模型):

  Client Request


  ┌──────────────┐
  │ Middleware 1  │ ── next() ──▶ ┌──────────────┐
  └──────────────┘                │ Middleware 2  │ ── next() ──▶ ┌──────────────┐
                                  └──────────────┘                │ Middleware 3  │
                                                                  │  res.send()   │
                                                                  └──────────────┘

控制权单向传递,不会回溯到 M1 或 M2

app.use() 将中间件函数推入内部的 stack 数组,当请求到达时,Express 从 stack[0] 开始依次执行,每个中间件决定是调用 next() 继续还是直接响应:

javascript
const express = require('express')
const app = express()

app.use((req, res, next) => {
  req.requestTime = Date.now()
  next()
})

app.use((req, res, next) => {
  console.log(`[${req.method}] ${req.url} - ${req.requestTime}`)
  next()
})

app.get('/', (req, res) => {
  res.json({ time: req.requestTime })
})

app.listen(3000)

中间件分类

Express 中间件按功能角色可分为五大类:

┌───────────────────────────────────────────────────────────────┐
│                     Express 中间件分类                          │
├───────────────┬───────────────────────────────────────────────┤
│  应用级中间件   │  app.use() / app.METHOD()                    │
├───────────────┼───────────────────────────────────────────────┤
│  路由级中间件   │  router.use() / router.METHOD()              │
├───────────────┼───────────────────────────────────────────────┤
│  错误处理中间件 │  (err, req, res, next) 四参数函数             │
├───────────────┼───────────────────────────────────────────────┤
│  内置中间件    │  express.json() / express.static()            │
├───────────────┼───────────────────────────────────────────────┤
│  第三方中间件   │  cors / helmet / morgan 等                    │
└───────────────┴───────────────────────────────────────────────┘

1.2 路由系统(Router / Route / Layer)

Express 的路由系统由三层核心结构组成,理解它们的关系是深入 Express 源码的关键:

┌──────────────────────────────────────────────────────────────────────┐
│                      Express 路由系统架构                              │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────┐        │
│  │                     Application                           │        │
│  │  app._router = Router 实例                                │        │
│  └──────────────────────┬───────────────────────────────────┘        │
│                         │                                            │
│                    app._router                                       │
│                         │                                            │
│  ┌──────────────────────▼───────────────────────────────────┐        │
│  │                     Router                                │        │
│  │  this.stack = [Layer, Layer, Layer, ...]                   │        │
│  │                                                           │        │
│  │  每个 Layer 可以是:                                       │        │
│  │    - 普通中间件 (app.use 注册)                             │        │
│  │    - 路由中间件 (app.get/post 注册)                        │        │
│  │    - 子 Router (express.Router() 挂载)                     │        │
│  └────────┬──────────────┬──────────────────┬───────────────┘        │
│           │              │                  │                        │
│  ┌────────▼─────┐ ┌──────▼───────┐  ┌──────▼──────────┐             │
│  │   Layer      │ │    Layer     │  │     Layer        │             │
│  │ path: '/'    │ │ path: '/api' │  │ path: '/users'   │             │
│  │ handle: fn   │ │ handle:      │  │ handle: Route    │             │
│  │ (中间件)     │ │  subRouter   │  │                  │             │
│  └──────────────┘ └──────────────┘  └───────┬──────────┘             │
│                                             │                        │
│                                    ┌────────▼──────────┐             │
│                                    │      Route        │             │
│                                    │ path: '/users'    │             │
│                                    │ stack: [Layer]    │             │
│                                    │                   │             │
│                                    │ Layer: GET  → fn  │             │
│                                    │ Layer: POST → fn  │             │
│                                    │ Layer: PUT  → fn  │             │
│                                    └───────────────────┘             │
└──────────────────────────────────────────────────────────────────────┘

关键概念

  • Router:中间件容器,维护一个 stack 数组存放 Layer
  • Route:一个路径上所有 HTTP 方法处理函数的集合
  • Layer:包装层,封装路径匹配正则 + 处理函数

路由匹配过程:请求到达时,Router 遍历 stack 中的 Layer,每个 Layer 用 path-to-regexp 编译的正则去匹配请求路径。匹配成功后,如果 Layer 关联的是 Route,则继续在 Route 内部按 HTTP 方法匹配对应的处理函数。

javascript
const express = require('express')
const router = express.Router()

router.param('userId', async (req, res, next, id) => {
  try {
    const user = await User.findById(id)
    if (!user) return res.status(404).json({ error: 'User not found' })
    req.targetUser = user
    next()
  } catch (err) {
    next(err)
  }
})

router.route('/:userId')
  .get((req, res) => {
    res.json(req.targetUser)
  })
  .put(async (req, res, next) => {
    try {
      Object.assign(req.targetUser, req.body)
      await req.targetUser.save()
      res.json(req.targetUser)
    } catch (err) {
      next(err)
    }
  })
  .delete(async (req, res, next) => {
    try {
      await req.targetUser.remove()
      res.status(204).end()
    } catch (err) {
      next(err)
    }
  })

module.exports = router

路由匹配支持多种模式:

javascript
router.get('/users/:id', handler)
router.get('/files/*', handler)
router.get(/.*fly$/, handler)

router.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params
  res.json({ userId, postId })
})

1.3 错误处理中间件

Express 通过四参数函数签名来识别错误处理中间件。当 next(err) 被调用时(传入了参数),Express 会跳过所有普通中间件,直接找到下一个四参数的错误处理中间件执行:

正常流程:  M1 → M2 → M3 → M4(路由处理)

                    next(err)

错误流程:              ▼
               跳过 M4 → ErrorHandler1 → ErrorHandler2
javascript
app.get('/api/users/:id', async (req, res, next) => {
  try {
    const user = await UserService.findById(req.params.id)
    if (!user) {
      const err = new Error('User not found')
      err.status = 404
      return next(err)
    }
    res.json(user)
  } catch (err) {
    next(err)
  }
})

const notFoundHandler = (req, res, next) => {
  const err = new Error(`Cannot ${req.method} ${req.url}`)
  err.status = 404
  next(err)
}

const validationErrorHandler = (err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: {
        message: 'Validation Failed',
        details: Object.values(err.errors).map(e => e.message)
      }
    })
  }
  next(err)
}

const genericErrorHandler = (err, req, res, next) => {
  const status = err.status || 500
  res.status(status).json({
    error: {
      message: status === 500 ? 'Internal Server Error' : err.message,
      code: err.code || 'UNKNOWN_ERROR'
    }
  })
}

app.use(notFoundHandler)
app.use(validationErrorHandler)
app.use(genericErrorHandler)

Express 4.x 不会自动捕获 async 函数抛出的错误。asyncHandler 包装器是社区的标准解法:

javascript
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next)
}

app.get('/api/data', asyncHandler(async (req, res) => {
  const data = await fetchData()
  res.json(data)
}))

Express 5.x 已原生支持 async 错误捕获,不再需要包装器。

1.4 手写简版 Express 中间件机制

理解 Express 中间件机制的最好方式是从零实现一个简化版:

javascript
const http = require('http')

class MiniExpress {
  constructor() {
    this.stack = []
    this.routes = { GET: [], POST: [], PUT: [], DELETE: [] }
  }

  use(path, fn) {
    if (typeof path === 'function') {
      fn = path
      path = '/'
    }
    this.stack.push({ path, handler: fn })
  }

  _addRoute(method, path, handler) {
    this.routes[method].push({ path, handler })
  }

  get(path, handler) { this._addRoute('GET', path, handler) }
  post(path, handler) { this._addRoute('POST', path, handler) }
  put(path, handler) { this._addRoute('PUT', path, handler) }
  delete(path, handler) { this._addRoute('DELETE', path, handler) }

  handle(req, res) {
    let idx = 0
    const stack = this.stack
    const routes = this.routes[req.method] || []

    const allLayers = [
      ...stack,
      ...routes.map(r => ({ path: r.path, handler: r.handler, exact: true }))
    ]

    const next = (err) => {
      if (err) {
        return this._handleError(err, req, res, allLayers, idx)
      }
      if (idx >= allLayers.length) {
        res.statusCode = 404
        res.end(JSON.stringify({ error: 'Not Found' }))
        return
      }

      const layer = allLayers[idx++]
      const url = req.url.split('?')[0]

      const match = layer.exact
        ? url === layer.path
        : url.startsWith(layer.path)

      if (match) {
        try {
          layer.handler(req, res, next)
        } catch (e) {
          next(e)
        }
      } else {
        next()
      }
    }

    next()
  }

  _handleError(err, req, res, layers, startIdx) {
    for (let i = startIdx; i < layers.length; i++) {
      const layer = layers[i]
      if (layer.handler.length === 4) {
        return layer.handler(err, req, res, (e) => {
          if (e) this._handleError(e, req, res, layers, i + 1)
        })
      }
    }
    res.statusCode = err.status || 500
    res.end(JSON.stringify({ error: err.message }))
  }

  listen(...args) {
    const server = http.createServer((req, res) => {
      res.json = (data) => {
        res.setHeader('Content-Type', 'application/json')
        res.end(JSON.stringify(data))
      }
      this.handle(req, res)
    })
    return server.listen(...args)
  }
}

核心原理一目了然:通过闭包维护索引 idx,每次调用 next() 时递增索引并执行下一个匹配的 Layer。遇到错误时跳过所有普通中间件(handler.length !== 4),找到四参数错误处理中间件执行。

使用示例:

javascript
const app = new MiniExpress()

app.use((req, res, next) => {
  req.startTime = Date.now()
  next()
})

app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`)
  next()
})

app.get('/', (req, res) => {
  res.json({ message: 'Hello MiniExpress', time: req.startTime })
})

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.statusCode = 500
  res.json({ error: err.message })
})

app.listen(3000)

2. Koa 框架

Koa 由 Express 的原班人马(TJ Holowaychuk)打造,是一个更轻量、更现代的 Web 框架。Koa 抛弃了回调风格,全面拥抱 async/await,并引入了著名的洋葱模型。Koa 本身极其精简——核心代码不到 2000 行,不捆绑任何中间件,连路由都需要第三方库。

2.1 洋葱模型原理与实现

Koa 的中间件执行遵循洋葱模型——请求从外层中间件进入,逐层穿透到核心,然后再从核心逐层返回。每个中间件在 await next() 前后分别处理"下行"和"上行"两个阶段:

                    请求 ──────────────────────────────▶ 响应
                    │                                     ▲
                    ▼                                     │
              ┌───────────────────────────────────────────────┐
              │           Middleware 1 (前置逻辑)              │
              │    ┌─────────────────────────────────────┐    │
              │    │       Middleware 2 (前置逻辑)         │    │
              │    │    ┌───────────────────────────┐    │    │
              │    │    │     Middleware 3 (核心)     │    │    │
              │    │    └───────────────────────────┘    │    │
              │    │       Middleware 2 (后置逻辑)         │    │
              │    └─────────────────────────────────────┘    │
              │           Middleware 1 (后置逻辑)              │
              └───────────────────────────────────────────────┘
javascript
const Koa = require('koa')
const app = new Koa()

app.use(async (ctx, next) => {
  console.log('1 - 进入')
  await next()
  console.log('1 - 返回')
})

app.use(async (ctx, next) => {
  console.log('2 - 进入')
  await next()
  console.log('2 - 返回')
})

app.use(async (ctx, next) => {
  console.log('3 - 核心处理')
  ctx.body = 'Hello Koa'
})

app.listen(3000)

执行顺序输出:

1 - 进入
2 - 进入
3 - 核心处理
2 - 返回
1 - 返回

洋葱模型的精妙之处在于:外层中间件可以精确测量内层中间件的执行耗时、捕获内层抛出的错误、对内层产生的响应进行二次加工。

javascript
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  ctx.set('X-Response-Time', `${duration}ms`)
})

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = { error: err.message }
    ctx.app.emit('error', err, ctx)
  }
})

2.2 koa-compose 源码解析与手写

koa-compose 是洋葱模型的核心实现,整个源码不到 50 行,是理解 Koa 最关键的一段代码:

javascript
function compose(middleware) {
  if (!Array.isArray(middleware)) {
    throw new TypeError('Middleware stack must be an array')
  }
  for (const fn of middleware) {
    if (typeof fn !== 'function') {
      throw new TypeError('Middleware must be composed of functions')
    }
  }

  return function (context, next) {
    let index = -1

    return dispatch(0)

    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'))
      }
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()

      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

逐行拆解核心逻辑:

  1. index = -1:记录上一次执行的中间件索引,防止 next() 被多次调用
  2. dispatch(0):从第一个中间件开始执行
  3. i <= index 检查:如果当前索引 ≤ 上次记录的索引,说明 next() 被重复调用
  4. fn(context, dispatch.bind(null, i + 1)):将 dispatch(i+1) 作为 next 传给当前中间件
  5. Promise.resolve() 包装:确保返回值始终是 Promise,支持 async/await

执行流可视化:

compose([m1, m2, m3])(ctx)


dispatch(0)


m1(ctx, dispatch(1))    ◄── m1 的 next = dispatch(1)
  │  await next()

  dispatch(1)


  m2(ctx, dispatch(2))  ◄── m2 的 next = dispatch(2)
    │  await next()

    dispatch(2)


    m3(ctx, dispatch(3)) ◄── m3 的 next = dispatch(3)
      │  (不调用 next 或调用后)

      dispatch(3)


      fn = undefined → Promise.resolve()  ◄── 到达终点,开始回溯

      m3 后续代码执行

    m2 的 await next() resolve,执行 m2 后续代码

  m1 的 await next() resolve,执行 m1 后续代码

2.3 Context 对象设计

Koa 将 Node.js 原生的 requestresponse 封装到一个 Context 对象中,并通过 delegates 模式将常用属性代理到 ctx 上:

┌──────────────────────────────────────────────────────────────┐
│                        ctx (Context)                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ctx.request   ──────▶  Koa Request (对 node req 的封装)      │
│  ctx.response  ──────▶  Koa Response (对 node res 的封装)     │
│  ctx.req       ──────▶  Node.js 原生 IncomingMessage          │
│  ctx.res       ──────▶  Node.js 原生 ServerResponse           │
│  ctx.state     ──────▶  推荐的命名空间,跨中间件传递数据         │
│  ctx.app       ──────▶  Application 实例引用                  │
│  ctx.cookies   ──────▶  Cookie 操作                          │
│                                                              │
│  代理属性(Delegates):                                       │
│  ctx.url        ═══▶  ctx.request.url                        │
│  ctx.method     ═══▶  ctx.request.method                     │
│  ctx.query      ═══▶  ctx.request.query                      │
│  ctx.body       ═══▶  ctx.response.body                      │
│  ctx.status     ═══▶  ctx.response.status                    │
│  ctx.set()      ═══▶  ctx.response.set()                     │
└──────────────────────────────────────────────────────────────┘

delegates 库的底层原理是通过 Object.defineProperty 将属性访问转发到目标对象:

javascript
const delegate = require('delegates')

const proto = {}

delegate(proto, 'response')
  .method('set')
  .method('redirect')
  .access('body')
  .access('status')
  .getter('headerSent')

delegate(proto, 'request')
  .method('accepts')
  .method('get')
  .access('url')
  .getter('query')
  .getter('path')
  .getter('ip')

ctx.state 是 Koa 推荐的跨中间件数据共享方式,避免直接在 ctx 上挂载属性造成命名冲突:

javascript
app.use(async (ctx, next) => {
  const token = ctx.get('Authorization')?.replace('Bearer ', '')
  if (token) {
    ctx.state.user = await verifyToken(token)
  }
  await next()
})

router.get('/api/profile', async (ctx) => {
  const { user } = ctx.state
  ctx.body = { user }
})

2.4 与 Express 中间件模型对比(线性 vs 洋葱)

┌────────────────────┬───────────────────────────┬───────────────────────────┐
│       特性          │        Express             │         Koa               │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   异步模型          │  Callback(next())         │  Promise(await next())   │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   执行模型          │  线性流(无回溯)            │  洋葱模型(有回溯)         │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   错误处理          │  next(err) + 四参数函数     │  try/catch 自然捕获        │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   响应时机          │  调用 res.send() 立即响应   │  所有中间件执行完后统一响应   │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   上下文对象        │  req + res 分离            │  ctx 统一封装              │
├────────────────────┼───────────────────────────┼───────────────────────────┤
│   后置处理能力      │  困难(需 hack)            │  天然支持(await next 后)  │
└────────────────────┴───────────────────────────┴───────────────────────────┘

Express 线性模型想实现"后置处理"需要借助事件监听等 hack 手段:

javascript
app.use((req, res, next) => {
  const start = Date.now()
  res.on('finish', () => {
    const duration = Date.now() - start
    console.log(`${req.method} ${req.url} - ${duration}ms`)
  })
  next()
})

Koa 的同样功能自然而优雅:

javascript
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  console.log(`${ctx.method} ${ctx.url} - ${duration}ms`)
})

Express 的回调模型在处理异步错误时需要显式调用 next(err),漏掉则会导致请求挂起。Koa 基于 Promise 链,中间件内的异步错误会自动沿 Promise 链传播,外层 try/catch 可以自然捕获。

2.5 手写迷你 Koa

javascript
const http = require('http')

class MiniKoa {
  constructor() {
    this.middleware = []
  }

  use(fn) {
    if (typeof fn !== 'function') {
      throw new TypeError('Middleware must be a function')
    }
    this.middleware.push(fn)
    return this
  }

  compose(middleware) {
    return function (ctx) {
      let index = -1

      function dispatch(i) {
        if (i <= index) {
          return Promise.reject(new Error('next() called multiple times'))
        }
        index = i
        const fn = middleware[i]
        if (!fn) return Promise.resolve()
        try {
          return Promise.resolve(fn(ctx, () => dispatch(i + 1)))
        } catch (err) {
          return Promise.reject(err)
        }
      }

      return dispatch(0)
    }
  }

  createContext(req, res) {
    const ctx = {
      req, res,
      method: req.method,
      url: req.url,
      path: req.url.split('?')[0],
      query: Object.fromEntries(new URLSearchParams(req.url.split('?')[1] || '')),
      headers: req.headers,
      status: 200,
      body: null,
      state: {},
      get(field) { return this.headers[field.toLowerCase()] },
      set(field, value) { this.res.setHeader(field, value) },
      throw(status, message) {
        const err = new Error(message || http.STATUS_CODES[status])
        err.status = status
        throw err
      }
    }
    return ctx
  }

  respond(ctx) {
    const { res, body, status } = ctx
    res.statusCode = status

    if (body === null || body === undefined) {
      res.statusCode = 204
      return res.end()
    }
    if (typeof body === 'string') {
      res.setHeader('Content-Type', 'text/plain; charset=utf-8')
      return res.end(body)
    }
    if (Buffer.isBuffer(body)) {
      res.setHeader('Content-Type', 'application/octet-stream')
      return res.end(body)
    }
    if (typeof body === 'object') {
      res.setHeader('Content-Type', 'application/json; charset=utf-8')
      return res.end(JSON.stringify(body))
    }
    res.end(String(body))
  }

  listen(...args) {
    const server = http.createServer((req, res) => {
      const ctx = this.createContext(req, res)
      const fn = this.compose(this.middleware)

      fn(ctx)
        .then(() => this.respond(ctx))
        .catch((err) => {
          ctx.status = err.status || 500
          ctx.body = { error: err.message }
          this.respond(ctx)
        })
    })
    return server.listen(...args)
  }
}

3. Nest.js 框架

Nest.js 是一个基于 TypeScript 的渐进式 Node.js 框架,深度借鉴了 Angular 和 Java Spring 的设计理念,通过装饰器、依赖注入、模块化等企业级架构模式,为 Node.js 后端开发带来了前所未有的工程化体验。Nest.js 底层可以使用 Express 或 Fastify 作为 HTTP 平台适配器。

3.1 依赖注入(DI)与控制反转(IoC)原理

依赖注入(Dependency Injection)是一种设计模式,其核心思想是控制反转(Inversion of Control)——应用程序不主动创建依赖,而是由外部容器负责创建和注入。

┌──────────────────────────────────────────────────────────────────┐
│               传统方式 vs IoC 方式                                 │
│                                                                  │
│  传统方式(强耦合):                                              │
│                                                                  │
│    class UserController {                                        │
│      constructor() {                                             │
│        this.service = new UserService()     ← 主动创建依赖        │
│        this.service.repo = new UserRepo()   ← 嵌套创建            │
│      }                                                           │
│    }                                                             │
│                                                                  │
│  IoC 方式(松耦合):                                              │
│                                                                  │
│    class UserController {                                        │
│      constructor(                                                │
│        private userService: UserService     ← 声明依赖,容器注入   │
│      ) {}                                                        │
│    }                                                             │
│                                                                  │
│  ┌──────────┐    依赖    ┌──────────┐    依赖    ┌──────────┐    │
│  │Controller│ ────────▶ │  Service │ ────────▶ │   Repo   │    │
│  └──────────┘           └──────────┘           └──────────┘    │
│       ▲                       ▲                    ▲            │
│       └──────── IoC 容器自动注入 ┴────────────────────┘            │
└──────────────────────────────────────────────────────────────────┘

Nest.js 的 IoC 容器工作流程:

  1. 扫描 @Module 声明的 providers
  2. 通过 reflect-metadata 分析每个 provider 构造函数的参数类型
  3. 递归解析依赖链
  4. 按依赖顺序实例化
  5. 注入到需要的地方

Nest.js 支持多种 Provider 注册方式:

typescript
@Module({
  providers: [
    UserService,

    {
      provide: 'CONFIG',
      useValue: {
        apiUrl: 'https://api.example.com',
        timeout: 5000,
      },
    },

    {
      provide: 'CONNECTION',
      useFactory: async (configService: ConfigService) => {
        const options = configService.get('database')
        return createConnection(options)
      },
      inject: [ConfigService],
    },

    {
      provide: 'LOGGER',
      useClass: process.env.NODE_ENV === 'production'
        ? ProductionLogger
        : DevelopmentLogger,
    },

    {
      provide: 'ALIAS_SERVICE',
      useExisting: UserService,
    },
  ],
})
export class AppModule {}

Provider 作用域控制:

┌──────────────┬──────────────────────────────────────────┐
│    作用域      │              说明                        │
├──────────────┼──────────────────────────────────────────┤
│   DEFAULT    │  单例,整个应用共享一个实例(默认)          │
├──────────────┼──────────────────────────────────────────┤
│   REQUEST    │  每个请求创建新实例,请求结束后销毁          │
├──────────────┼──────────────────────────────────────────┤
│  TRANSIENT   │  每次注入都创建新实例,不共享               │
└──────────────┴──────────────────────────────────────────┘

3.2 装饰器(Decorator)机制

Nest.js 大量使用 TypeScript 装饰器来声明路由、注入依赖、定义模块。装饰器底层依赖 reflect-metadata 库实现元数据的存取。

┌─────────────────────────────────────────────────────────────┐
│                    Nest.js 核心装饰器                         │
├─────────────┬───────────────────────────────────────────────┤
│  @Controller │  声明控制器类,接收路由前缀                     │
├─────────────┼───────────────────────────────────────────────┤
│  @Injectable │  声明可注入的服务(Provider)                   │
├─────────────┼───────────────────────────────────────────────┤
│  @Module     │  声明模块,组织 Controller/Provider/Import     │
├─────────────┼───────────────────────────────────────────────┤
│  @Get/@Post  │  声明 HTTP 方法路由                           │
├─────────────┼───────────────────────────────────────────────┤
│  @Body       │  提取请求体                                   │
├─────────────┼───────────────────────────────────────────────┤
│  @Param      │  提取路由参数                                 │
├─────────────┼───────────────────────────────────────────────┤
│  @Query      │  提取查询参数                                 │
├─────────────┼───────────────────────────────────────────────┤
│  @UseGuards  │  绑定守卫                                     │
├─────────────┼───────────────────────────────────────────────┤
│  @UsePipes   │  绑定管道                                     │
└─────────────┴───────────────────────────────────────────────┘

装饰器的本质是利用 reflect-metadata 在类上存储元数据。Nest.js 在启动时读取这些元数据来构建路由表和依赖图:

typescript
import 'reflect-metadata'

function Controller(prefix: string = ''): ClassDecorator {
  return (target) => {
    Reflect.defineMetadata('prefix', prefix, target)
  }
}

function Get(path: string = ''): MethodDecorator {
  return (target, propertyKey, descriptor) => {
    Reflect.defineMetadata('method', 'GET', descriptor.value!)
    Reflect.defineMetadata('path', path, descriptor.value!)
  }
}

function Injectable(): ClassDecorator {
  return (target) => {
    Reflect.defineMetadata('injectable', true, target)
  }
}

3.3 Module / Controller / Service 架构

┌─────────────────────────────────────────────────────────┐
│                       AppModule                          │
│  ┌─────────────┐  ┌──────────────┐  ┌───────────────┐  │
│  │  AuthModule  │  │  UserModule  │  │  OrderModule  │  │
│  └──────┬──────┘  └──────┬───────┘  └───────┬───────┘  │
│         │                │                   │          │
│         ▼                ▼                   ▼          │
│  ┌─────────────────────────────────────────────────┐    │
│  │              SharedModule (共享模块)               │    │
│  │   DatabaseModule / LoggerModule / ConfigModule   │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘
typescript
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll(@Query() query: QueryUserDto) {
    return this.userService.findAll(query)
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id)
  }

  @Post()
  @HttpCode(201)
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto)
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    return this.userService.update(id, dto)
  }

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id: string) {
    return this.userService.remove(id)
  }
}
typescript
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}

  async findAll(query: QueryUserDto) {
    const { page = 1, limit = 20, keyword } = query
    const qb = this.userRepo.createQueryBuilder('user')
    if (keyword) {
      qb.where('user.name LIKE :keyword', { keyword: `%${keyword}%` })
    }
    const [items, total] = await qb
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount()
    return { items, total, page, limit }
  }

  async findOne(id: string) {
    const user = await this.userRepo.findOne({ where: { id } })
    if (!user) throw new NotFoundException(`User #${id} not found`)
    return user
  }

  async create(dto: CreateUserDto) {
    const user = this.userRepo.create(dto)
    return this.userRepo.save(user)
  }

  async update(id: string, dto: UpdateUserDto) {
    const user = await this.findOne(id)
    Object.assign(user, dto)
    return this.userRepo.save(user)
  }

  async remove(id: string) {
    const user = await this.findOne(id)
    return this.userRepo.remove(user)
  }
}
typescript
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

动态模块——允许在导入时传入配置:

typescript
@Global()
@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        {
          provide: 'DATABASE_CONNECTION',
          useFactory: async () => createConnection(options),
        },
      ],
      exports: ['DATABASE_CONNECTION'],
    }
  }
}

3.4 Guard / Interceptor / Pipe / Filter 生命周期

Nest.js 的请求处理管线包含多个层次,每一层都有特定的职责。理解请求在这些层次中的流转顺序,是掌握 Nest.js 的关键:

客户端请求


┌─────────────────┐
│    Middleware    │  最先执行,类似 Express 中间件
└────────┬────────┘

┌─────────────────┐
│     Guards      │  鉴权守卫,返回 true/false 决定是否放行
└────────┬────────┘

┌─────────────────┐
│  Interceptors   │  拦截器(前置逻辑),类似 AOP 的 Before
│   (Before)      │
└────────┬────────┘

┌─────────────────┐
│     Pipes       │  数据转换与校验,处理 Handler 参数
└────────┬────────┘

┌─────────────────┐
│    Handler      │  路由处理函数(Controller 方法)
└────────┬────────┘

┌─────────────────┐
│  Interceptors   │  拦截器(后置逻辑),类似 AOP 的 After
│   (After)       │
└────────┬────────┘

┌─────────────────┐
│Exception Filter │  异常过滤器(当任何层抛出异常时触发)
└────────┬────────┘

    服务端响应

Guard(守卫)

typescript
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private readonly reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler())
    if (isPublic) return true

    const request = context.switchToHttp().getRequest()
    const token = request.headers.authorization?.replace('Bearer ', '')
    if (!token) throw new UnauthorizedException('Token is required')

    try {
      request.user = await this.jwtService.verifyAsync(token)
      return true
    } catch {
      throw new UnauthorizedException('Invalid token')
    }
  }
}

Interceptor(拦截器)

Interceptor 可以在 Handler 执行前后添加逻辑,基于 RxJS Observable 实现,类似 Koa 的洋葱模型:

typescript
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        code: 0,
        message: 'success',
        data,
        timestamp: Date.now(),
      })),
    )
  }
}

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest()
    const { method, url } = request
    const now = Date.now()
    return next.handle().pipe(
      tap(() => console.log(`${method} ${url} - ${Date.now() - now}ms`)),
    )
  }
}

Pipe(管道)

typescript
@Injectable()
export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown, metadata: ArgumentMetadata) {
    const result = this.schema.safeParse(value)
    if (!result.success) {
      throw new BadRequestException({
        message: 'Validation failed',
        errors: result.error.flatten().fieldErrors,
      })
    }
    return result.data
  }
}

ExceptionFilter(异常过滤器)

typescript
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name)

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const request = ctx.getRequest<Request>()
    const response = ctx.getResponse<Response>()

    let status = HttpStatus.INTERNAL_SERVER_ERROR
    let message = 'Internal server error'

    if (exception instanceof HttpException) {
      status = exception.getStatus()
      const resp = exception.getResponse()
      message = typeof resp === 'string' ? resp : (resp as any).message
    }

    if (status >= 500) {
      this.logger.error(`${request.method} ${request.url}`,
        exception instanceof Error ? exception.stack : String(exception))
    }

    response.status(status).json({
      code: -1,
      message,
      timestamp: new Date().toISOString(),
      path: request.url,
    })
  }
}

3.5 与 Spring Boot 的对比

Nest.js 的设计深受 Java Spring 框架的影响,两者在核心架构概念上高度相似:

┌──────────────────┬──────────────────────┬──────────────────────┐
│     概念          │       Nest.js         │     Spring Boot      │
├──────────────────┼──────────────────────┼──────────────────────┤
│  入口装饰器       │  @Controller          │  @RestController     │
├──────────────────┼──────────────────────┼──────────────────────┤
│  服务层           │  @Injectable          │  @Service/@Component │
├──────────────────┼──────────────────────┼──────────────────────┤
│  模块化           │  @Module              │  @Configuration      │
├──────────────────┼──────────────────────┼──────────────────────┤
│  依赖注入         │  构造函数注入          │  构造函数/@Autowired  │
├──────────────────┼──────────────────────┼──────────────────────┤
│  路由定义         │  @Get/@Post 等        │  @GetMapping 等      │
├──────────────────┼──────────────────────┼──────────────────────┤
│  参数提取         │  @Body/@Param/@Query  │  @RequestBody 等     │
├──────────────────┼──────────────────────┼──────────────────────┤
│  守卫/拦截        │  Guard / Interceptor  │  Filter / Interceptor│
├──────────────────┼──────────────────────┼──────────────────────┤
│  数据校验         │  Pipe + class-validator│ @Valid + Hibernate   │
├──────────────────┼──────────────────────┼──────────────────────┤
│  异常处理         │  ExceptionFilter      │  @ExceptionHandler   │
├──────────────────┼──────────────────────┼──────────────────────┤
│  AOP 切面        │  Interceptor          │  @Aspect + AOP 代理   │
├──────────────────┼──────────────────────┼──────────────────────┤
│  作用域           │  DEFAULT/REQUEST      │  Singleton/Prototype │
├──────────────────┼──────────────────────┼──────────────────────┤
│  配置管理         │  ConfigModule         │  @Value / application│
│                  │                      │  .properties         │
├──────────────────┼──────────────────────┼──────────────────────┤
│  微服务           │  @nestjs/microservices│  Spring Cloud        │
├──────────────────┼──────────────────────┼──────────────────────┤
│  ORM             │  TypeORM / Prisma     │  JPA / Hibernate     │
├──────────────────┼──────────────────────┼──────────────────────┤
│  测试             │  Jest + @nestjs/testing│ JUnit + Mockito     │
└──────────────────┴──────────────────────┴──────────────────────┘

共同的设计哲学:

  1. 控制反转(IoC):应用程序不主动创建依赖,由容器负责管理对象生命周期
  2. 面向切面编程(AOP):通过拦截器/守卫实现横切关注点(日志、鉴权、缓存)的解耦
  3. 装饰器/注解驱动:通过声明式元数据标注来定义行为,减少样板代码
  4. 模块化封装:功能按领域模型划分为独立模块,模块间通过导入导出通信

关键差异:

  • Spring Boot 的 IoC 容器(ApplicationContext)功能更强大,支持 Bean 生命周期回调、条件注入(@Conditional)、AOP 代理等高级特性
  • Nest.js 的 IoC 容器更轻量,依赖 TypeScript 编译期类型信息(emitDecoratorMetadata),而 Spring 使用运行时反射 + CGLIB 动态代理
  • Spring Boot 有成熟的事务管理(@Transactional),Nest.js 需要借助 TypeORM/Prisma 自行管理

4. 框架选型对比

4.1 Express vs Koa vs Nest.js vs Fastify 特性对比

┌──────────────┬────────────┬────────────┬────────────────┬──────────────┐
│     维度      │  Express   │    Koa     │    Nest.js     │   Fastify    │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  发布年份     │   2010     │    2013    │     2017       │    2016      │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  设计理念     │  简约灵活   │  轻量优雅   │  企业级架构     │  极致性能     │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  语言        │ JavaScript │ JavaScript │  TypeScript    │ JavaScript/  │
│              │            │            │               │ TypeScript   │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  异步模型     │  Callback  │ async/await│  async/await   │ async/await  │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  中间件模型   │  线性流     │  洋葱模型   │  管线 + 洋葱   │  生命周期钩子 │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  路由系统     │  内置       │ koa-router │  装饰器声明式   │  内置(Radix  │
│              │            │            │               │  Tree)       │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  JSON 序列化  │  较慢       │  较慢      │  取决于底层     │  fast-json-  │
│              │            │            │               │  stringify   │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  Schema 校验  │  第三方     │  第三方     │  内置(Pipe)    │  内置(JSON   │
│              │            │            │               │  Schema)     │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  依赖注入     │   无        │   无       │  内置 IoC 容器  │    无        │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  TypeScript   │  需配置     │  需配置     │  原生支持      │  良好支持    │
│  支持         │            │            │               │             │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  性能(req/s)  │  ~15,000   │  ~18,000  │  ~15,000       │  ~45,000+   │
│  (hello world)│            │            │ (Express适配)  │             │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  包体积       │  ~200KB    │  ~20KB     │  ~10MB+        │  ~2MB       │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  NPM 周下载   │  ~30M      │  ~2.5M     │  ~3M          │  ~2.5M      │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  生态规模     │  最大       │  中等      │  快速增长       │  较好        │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  学习曲线     │   低        │  低        │  高            │  中          │
├──────────────┼────────────┼────────────┼────────────────┼──────────────┤
│  底层平台     │  自有       │  自有      │ Express/Fastify│  自有        │
└──────────────┴────────────┴────────────┴────────────────┴──────────────┘

4.2 Fastify 核心特性

Fastify 的性能优势主要来自三个方面:

┌───────────────────────────────────────────────────────────────────┐
│                    Fastify 性能优化策略                             │
│                                                                   │
│  1. Radix Tree 路由                                               │
│     ├── find-my-way 库实现                                        │
│     ├── 路由查找时间复杂度 O(k),k 为路径长度                       │
│     └── 相比 Express 的正则逐一匹配,大规模路由下优势显著             │
│                                                                   │
│  2. JSON 序列化加速                                                │
│     ├── fast-json-stringify 库                                    │
│     ├── 基于 JSON Schema 预编译序列化函数                           │
│     └── 比 JSON.stringify 快 2-5 倍                               │
│                                                                   │
│  3. Schema 驱动开发                                                │
│     ├── 请求/响应都可以定义 JSON Schema                             │
│     ├── 自动生成 Swagger 文档                                      │
│     └── 编译时校验,运行时零开销                                    │
└───────────────────────────────────────────────────────────────────┘
javascript
const fastify = require('fastify')({ logger: true })

const getUserSchema = {
  params: {
    type: 'object',
    properties: {
      id: { type: 'string', pattern: '^[0-9a-f]{24}$' }
    },
    required: ['id']
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        email: { type: 'string' }
      }
    }
  }
}

fastify.get('/users/:id', { schema: getUserSchema }, async (request, reply) => {
  const user = await UserService.findById(request.params.id)
  return user
})

fastify.register(require('@fastify/cors'), { origin: true })
fastify.register(require('@fastify/helmet'))

fastify.listen({ port: 3000 })

Fastify 的生命周期钩子系统(替代传统中间件):

Request ──▶ onRequest ──▶ preParsing ──▶ preValidation ──▶ preHandler

Response ◀── onSend ◀── preSerialization ◀── Handler ◀────────┘


 onResponse

4.3 适用场景分析

┌─────────────────────────────────────────────────────────────────────┐
│                         框架选择决策树                                │
│                                                                     │
│  你的项目需要什么?                                                   │
│    │                                                                │
│    ├─▶ 快速原型 / 小型 API ──────────▶ Express                      │
│    │     社区资源丰富,入门门槛最低                                    │
│    │                                                                │
│    ├─▶ 中型项目,追求代码优雅 ──────▶ Koa                            │
│    │     洋葱模型天然适合日志/错误处理                                  │
│    │                                                                │
│    ├─▶ 极致性能要求 ──────────────▶ Fastify                         │
│    │     高吞吐 API 网关、计算密集型微服务                              │
│    │                                                                │
│    ├─▶ 大型企业项目 ──────────────▶ Nest.js                         │
│    │     强类型 + 模块化 + DI + 统一规范                               │
│    │                                                                │
│    ├─▶ 微服务架构 ──────────────▶ Nest.js / Fastify                 │
│    │     Nest.js 内置微服务支持(TCP/Redis/gRPC)                      │
│    │     Fastify 轻量高性能                                          │
│    │                                                                │
│    └─▶ 团队有 Spring/Angular 背景 ─▶ Nest.js                       │
│          概念迁移成本最低                                             │
└─────────────────────────────────────────────────────────────────────┘

5. 中间件进阶

5.1 常用中间件详解

body-parser

Express 4.16+ 已内置 express.json()express.urlencoded(),本质就是 body-parser。它负责解析请求体并将结果挂载到 req.body 上。

javascript
const express = require('express')
const app = express()

app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true, limit: '10mb' }))
app.use(express.raw({ type: 'application/octet-stream', limit: '50mb' }))
app.use(express.text({ type: 'text/plain' }))

内部工作原理:监听 reqdata 事件收集 Buffer 片段,end 事件触发时拼接完整 Buffer,根据 Content-Type 解析为 JSON/URL-encoded/Raw/Text 格式。

cors

CORS(Cross-Origin Resource Sharing)中间件处理跨域请求。核心是在响应头中添加 Access-Control-Allow-* 系列字段:

javascript
const cors = require('cors')

app.use(cors({
  origin: ['https://example.com', 'https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
  exposedHeaders: ['X-Total-Count', 'X-Response-Time'],
  credentials: true,
  maxAge: 86400,
}))
┌──────────────────────────────────────────────────────────────────┐
│                    CORS 预检请求流程                               │
│                                                                  │
│  浏览器                             服务器                        │
│    │                                  │                          │
│    │── OPTIONS /api/data ───────────▶ │  预检请求                 │
│    │   Origin: https://app.com        │                          │
│    │   Access-Control-Request-Method  │                          │
│    │                                  │                          │
│    │◀─── 200 OK ─────────────────── │  返回允许的方法/头          │
│    │   Access-Control-Allow-Origin   │                          │
│    │   Access-Control-Allow-Methods  │                          │
│    │   Access-Control-Max-Age        │                          │
│    │                                  │                          │
│    │── POST /api/data ─────────────▶ │  实际请求                 │
│    │   Origin: https://app.com        │                          │
│    │                                  │                          │
│    │◀─── 200 OK + data ──────────── │                          │
│    │   Access-Control-Allow-Origin   │                          │
└──────────────────────────────────────────────────────────────────┘

helmet

Helmet 通过设置一系列 HTTP 安全头来保护应用:

javascript
const helmet = require('helmet')

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
    },
  },
  crossOriginEmbedderPolicy: false,
  hsts: { maxAge: 31536000, includeSubDomains: true },
}))
Helmet 设置的安全头:
┌──────────────────────────────────┬──────────────────────────────────┐
│  X-Content-Type-Options: nosniff │  防止 MIME 类型嗅探               │
│  X-Frame-Options: DENY           │  防止点击劫持(Clickjacking)     │
│  X-XSS-Protection: 0             │  禁用旧版 XSS 过滤器              │
│  Strict-Transport-Security       │  强制 HTTPS                      │
│  Content-Security-Policy         │  内容安全策略,防止 XSS/注入       │
│  X-DNS-Prefetch-Control: off     │  控制 DNS 预取                   │
│  Referrer-Policy: no-referrer    │  控制 Referer 头发送策略           │
└──────────────────────────────────┴──────────────────────────────────┘

morgan

HTTP 请求日志中间件,支持预定义和自定义格式:

javascript
const morgan = require('morgan')
const fs = require('fs')
const path = require('path')

app.use(morgan('dev'))

const accessLogStream = fs.createWriteStream(
  path.join(__dirname, 'access.log'),
  { flags: 'a' }
)

app.use(morgan('combined', {
  stream: accessLogStream,
  skip: (req, res) => res.statusCode < 400,
}))

morgan.token('request-id', (req) => req.headers['x-request-id'])
morgan.token('user-id', (req) => req.user?.id || 'anonymous')

app.use(morgan(':request-id :user-id :method :url :status :response-time ms'))

compression

Gzip/Brotli 压缩响应体,减少网络传输体积:

javascript
const compression = require('compression')

app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false
    return compression.filter(req, res)
  },
}))

5.2 中间件执行顺序的陷阱

中间件顺序在 Express/Koa 中至关重要,错误的顺序会导致难以排查的 Bug:

陷阱一:body-parser 放在路由后面

javascript
app.post('/api/users', (req, res) => {
  console.log(req.body)
})

app.use(express.json())

req.bodyundefined,因为 JSON 解析中间件在路由之后才注册。

陷阱二:错误处理中间件放在路由前面

javascript
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message })
})

app.get('/api/data', asyncHandler(async (req, res) => {
  throw new Error('Something went wrong')
}))

错误处理中间件必须放在所有路由和中间件的最后面,否则无法捕获后续路由中的错误。

陷阱三:CORS 中间件放在鉴权后面

javascript
app.use(authMiddleware)
app.use(cors())

浏览器的 OPTIONS 预检请求不会携带 Authorization 头,导致预检请求被鉴权中间件拦截返回 401。CORS 中间件应该放在鉴权之前。

陷阱四:静态文件中间件的位置

javascript
app.use(authMiddleware)
app.use(express.static('public'))

静态文件(CSS/JS/图片)也会经过鉴权中间件,导致未登录用户无法加载页面资源。

正确的中间件注册顺序:

javascript
app.use(compression())
app.use(helmet())
app.use(cors())
app.use(morgan('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(express.static('public'))

app.use('/api', authMiddleware)
app.use('/api/v1', v1Router)
app.use('/api/v2', v2Router)

app.use(notFoundHandler)
app.use(errorHandler)

5.3 自定义中间件实战

请求日志中间件

javascript
function requestLogger(options = {}) {
  const {
    excludePaths = ['/health', '/metrics'],
    slowThreshold = 1000,
  } = options

  return async (req, res, next) => {
    if (excludePaths.includes(req.path)) return next()

    const requestId = req.headers['x-request-id'] || crypto.randomUUID()
    req.requestId = requestId
    res.setHeader('X-Request-Id', requestId)

    const start = process.hrtime.bigint()
    const { method, url, ip } = req

    res.on('finish', () => {
      const duration = Number(process.hrtime.bigint() - start) / 1e6
      const { statusCode } = res

      const logData = {
        requestId,
        method,
        url,
        ip,
        statusCode,
        duration: `${duration.toFixed(2)}ms`,
        userAgent: req.get('User-Agent'),
        contentLength: res.get('Content-Length'),
      }

      if (duration > slowThreshold) {
        console.warn('[SLOW REQUEST]', JSON.stringify(logData))
      } else if (statusCode >= 400) {
        console.error('[ERROR REQUEST]', JSON.stringify(logData))
      } else {
        console.log('[REQUEST]', JSON.stringify(logData))
      }
    })

    next()
  }
}

app.use(requestLogger({ slowThreshold: 2000 }))

权限校验中间件

javascript
function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' })
    }

    if (allowedRoles.length === 0) return next()

    const hasRole = allowedRoles.some(role => req.user.roles?.includes(role))
    if (!hasRole) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: allowedRoles,
        current: req.user.roles,
      })
    }

    next()
  }
}

app.get('/api/admin/users', authorize('admin', 'super-admin'), (req, res) => {
  res.json({ users: [] })
})

app.delete('/api/admin/users/:id', authorize('super-admin'), (req, res) => {
  res.status(204).end()
})

限流中间件(滑动窗口算法)

javascript
function rateLimit(options = {}) {
  const {
    windowMs = 60 * 1000,
    max = 100,
    keyGenerator = (req) => req.ip,
    message = 'Too many requests, please try again later',
  } = options

  const hits = new Map()

  setInterval(() => {
    const now = Date.now()
    for (const [key, records] of hits) {
      const valid = records.filter(t => now - t < windowMs)
      if (valid.length === 0) {
        hits.delete(key)
      } else {
        hits.set(key, valid)
      }
    }
  }, windowMs)

  return (req, res, next) => {
    const key = keyGenerator(req)
    const now = Date.now()

    if (!hits.has(key)) {
      hits.set(key, [])
    }

    const records = hits.get(key).filter(t => now - t < windowMs)
    records.push(now)
    hits.set(key, records)

    res.setHeader('X-RateLimit-Limit', max)
    res.setHeader('X-RateLimit-Remaining', Math.max(0, max - records.length))
    res.setHeader('X-RateLimit-Reset', new Date(now + windowMs).toISOString())

    if (records.length > max) {
      return res.status(429).json({ error: message })
    }

    next()
  }
}

app.use('/api', rateLimit({ windowMs: 60000, max: 100 }))
app.use('/api/auth/login', rateLimit({ windowMs: 300000, max: 5 }))

6. 路由设计最佳实践

6.1 RESTful 路由命名规范

┌───────────────────────────────────────────────────────────────────────────┐
│                       RESTful API 设计规范                                 │
├───────────────────────────────────────────────────────────────────────────┤
│                                                                           │
│  ✅ 正确                              ❌ 错误                             │
│  GET    /api/users                   GET    /api/getUsers                │
│  GET    /api/users/:id               GET    /api/getUserById             │
│  POST   /api/users                   POST   /api/createUser             │
│  PUT    /api/users/:id               POST   /api/updateUser             │
│  DELETE /api/users/:id               GET    /api/deleteUser?id=1        │
│                                                                           │
│  规则总结:                                                                │
│  1. 资源名用名词复数(users 不是 user)                                     │
│  2. 操作通过 HTTP Method 区分,不要把动词放在 URL 中                        │
│  3. 层级关系用嵌套路径表示                                                  │
│  4. URL 用小写 + 连字符(kebab-case)                                      │
│  5. 查询参数用于过滤/分页/排序                                              │
│                                                                           │
│  嵌套资源示例:                                                            │
│  GET    /api/users/:userId/posts          获取用户的文章列表                │
│  POST   /api/users/:userId/posts          为用户创建文章                   │
│  GET    /api/users/:userId/posts/:postId  获取具体文章                     │
│                                                                           │
│  查询参数示例:                                                            │
│  GET    /api/users?page=1&limit=20&sort=-createdAt&role=admin            │
│  GET    /api/posts?author=123&status=published&fields=title,summary      │
│                                                                           │
│  非 CRUD 操作的处理:                                                      │
│  POST   /api/users/:id/activate           激活用户(动作型操作用 POST)     │
│  POST   /api/orders/:id/cancel            取消订单                        │
│  POST   /api/articles/:id/publish         发布文章                        │
└───────────────────────────────────────────────────────────────────────────┘

6.2 路由分组与版本控制

Express

javascript
const express = require('express')
const app = express()

const v1Router = express.Router()
const v2Router = express.Router()

const userRouter = express.Router()
userRouter.get('/', userController.list)
userRouter.get('/:id', userController.findOne)
userRouter.post('/', userController.create)
userRouter.put('/:id', userController.update)
userRouter.delete('/:id', userController.remove)

const postRouter = express.Router()
postRouter.get('/', postController.list)
postRouter.get('/:id', postController.findOne)
postRouter.post('/', authMiddleware, postController.create)

v1Router.use('/users', userRouter)
v1Router.use('/posts', postRouter)

app.use('/api/v1', v1Router)
app.use('/api/v2', v2Router)

Koa

javascript
const Router = require('@koa/router')

const router = new Router({ prefix: '/api/v1' })

const userRoutes = new Router({ prefix: '/users' })
userRoutes.get('/', userController.list)
userRoutes.get('/:id', userController.findOne)
userRoutes.post('/', userController.create)

router.use(userRoutes.routes(), userRoutes.allowedMethods())

app.use(router.routes())
app.use(router.allowedMethods())

Nest.js

typescript
@Module({
  imports: [
    UserModule,
    PostModule,
    RouterModule.register([
      {
        path: 'api/v1',
        children: [
          { path: 'users', module: UserModule },
          { path: 'posts', module: PostModule },
        ],
      },
    ]),
  ],
})
export class AppModule {}

API 版本控制策略:

┌────────────────┬──────────────────────────────┬──────────────────────┐
│    策略         │         示例                  │       适用场景        │
├────────────────┼──────────────────────────────┼──────────────────────┤
│  URL 路径版本   │  /api/v1/users               │  最直观,适合公开 API │
├────────────────┼──────────────────────────────┼──────────────────────┤
│  Header 版本   │  Accept: application/vnd.     │  URL 保持简洁         │
│                │  api.v1+json                 │                      │
├────────────────┼──────────────────────────────┼──────────────────────┤
│  Query 版本    │  /api/users?version=1         │  简单但不够优雅        │
├────────────────┼──────────────────────────────┼──────────────────────┤
│  Media Type    │  Content-Type: application/  │  最符合 REST 规范     │
│                │  vnd.company.v1+json         │                      │
└────────────────┴──────────────────────────────┴──────────────────────┘

6.3 参数校验(Joi / Zod / class-validator)

三种主流参数校验方案对比:

┌──────────────────┬──────────────┬──────────────┬──────────────────┐
│     特性          │     Joi      │     Zod      │  class-validator │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  编程范式         │  链式 Schema  │  链式 Schema  │  装饰器           │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  TypeScript 支持  │  类型需手写   │  自动推导      │  需 class-       │
│                  │              │              │  transformer     │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  类型推导         │  ❌           │  ✅ z.infer   │  ❌ (class 即类型) │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  包体积           │  较大(~150KB) │  轻量(~50KB)  │  中等(~80KB)     │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  生态位           │  Hapi 生态    │  全栈通用      │  Nest.js 首选    │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  错误信息自定义    │  ✅ 非常灵活  │  ✅ 灵活      │  ✅ 装饰器参数    │
├──────────────────┼──────────────┼──────────────┼──────────────────┤
│  适用框架         │  Express/Hapi│  全部          │  Nest.js         │
└──────────────────┴──────────────┴──────────────┴──────────────────┘

Joi

javascript
const Joi = require('joi')

const createUserSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string()
    .min(8)
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .required(),
  age: Joi.number().integer().min(0).max(200),
  role: Joi.string().valid('user', 'admin', 'editor').default('user'),
  tags: Joi.array().items(Joi.string()).max(10),
})

const validateJoi = (schema) => (req, res, next) => {
  const { error, value } = schema.validate(req.body, {
    abortEarly: false,
    stripUnknown: true,
  })
  if (error) {
    return res.status(400).json({
      error: 'Validation failed',
      details: error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message,
      })),
    })
  }
  req.body = value
  next()
}

app.post('/api/users', validateJoi(createUserSchema), userController.create)

Zod

typescript
import { z } from 'zod'

const createUserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  password: z.string()
    .min(8)
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  age: z.number().int().min(0).max(200).optional(),
  role: z.enum(['user', 'admin', 'editor']).default('user'),
  tags: z.array(z.string()).max(10).optional(),
})

type CreateUserDto = z.infer<typeof createUserSchema>

const validateZod = (schema: z.ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
  const result = schema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.flatten().fieldErrors,
    })
  }
  req.body = result.data
  next()
}

app.post('/api/users', validateZod(createUserSchema), userController.create)

class-validator

typescript
import {
  IsString, IsEmail, IsOptional, IsInt, Min, Max,
  MinLength, MaxLength, IsEnum, IsArray, Matches,
} from 'class-validator'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
  password: string

  @IsOptional()
  @IsInt()
  @Min(0)
  @Max(200)
  age?: number

  @IsEnum(['user', 'admin', 'editor'])
  role: string = 'user'

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  tags?: string[]
}

在 Nest.js 中全局启用 ValidationPipe

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: {
      enableImplicitConversion: true,
    },
  }))

  await app.listen(3000)
}

whitelist: true 自动剥离 DTO 中未声明的属性,forbidNonWhitelisted: true 在遇到未声明属性时直接报错,这是防范恶意字段注入的关键配置。


7. 面试高频问题

问题一:Express 中间件的执行顺序是怎样的?如果不调用 next() 会发生什么?

Express 中间件按照 app.use() 注册的顺序依次执行。每个中间件通过调用 next() 将控制权传递给下一个中间件。

如果不调用 next(),请求处理链在当前中间件终止。如果该中间件也没有发送响应(res.send() / res.json() 等),请求会一直挂起直到客户端超时。

要点补充:

  • next() 本质是调用栈中下一个 Layer 的 handle 函数
  • 错误处理中间件通过 next(err) 触发,跳过所有普通中间件直达四参数错误处理函数
  • 重复调用 res.send() 会抛出 "Cannot set headers after they are sent" 错误

问题二:Koa 的洋葱模型和 Express 的线性模型有什么本质区别?

核心区别在于 next() 的返回值和执行语义:

  • Expressnext() 是普通函数调用,控制权单向传递,没有"回程"。next() 之后的代码会立即执行,不会等待下游中间件完成
  • Koanext() 返回 Promise,通过 await next() 等待下游中间件全部执行完后再执行后续逻辑

这使得 Koa 天然适合实现耗时统计、统一响应包装、错误捕获等横切关注点。Express 实现类似功能需要借助 res.on('finish') 事件监听。

追问思路:koa-compose 的实现原理、为什么要检测 next() 多次调用、Express 5.x 在这方面有什么改进。

问题三:Nest.js 的依赖注入是如何工作的?

Nest.js 的 DI 基于 TypeScript 装饰器和 reflect-metadata

  1. @Injectable() 装饰类时,TypeScript 编译器(需开启 emitDecoratorMetadata)将构造函数参数类型信息以元数据形式保存
  2. @Moduleproviders 声明注册可注入的 Provider
  3. IoC 容器启动时,通过 Reflect.getMetadata('design:paramtypes', target) 读取构造函数参数类型
  4. 递归解析依赖链,按依赖顺序实例化
  5. 默认单例作用域,一次实例化后全局复用

追问思路:如何实现循环依赖(forwardRef)、REQUEST 作用域的使用场景和注意事项、自定义 Provider 的几种方式(useValue/useFactory/useClass/useExisting)。

问题四:如何在 Express 中优雅处理异步错误?为什么需要 asyncHandler?

Express 4.x 的路由处理函数如果是 async 函数,抛出的错误不会被自动捕获。这是因为 Express 内部只做了同步 try/catch,而 async 函数返回的 rejected Promise 不在其捕获范围内。

解决方案:

javascript
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next)

asyncHandler 将 async 函数的返回值用 Promise.resolve 包裹,通过 .catch(next) 将 rejected Promise 转为 next(err) 调用,使其进入 Express 的错误处理流程。

Koa 不需要 asyncHandler,因为 koa-compose 已经用 Promise.resolve() 包装了每个中间件返回值。Express 5.x 也已原生支持。

问题五:请解释 Nest.js 请求生命周期中各组件的执行顺序和职责

Middleware → Guard → Interceptor(前) → Pipe → Handler → Interceptor(后) → ExceptionFilter
组件职责关键特征
Middleware通用请求处理与 Express 中间件一致,无法访问 ExecutionContext
Guard鉴权与访问控制返回 boolean,可访问装饰器元数据(Reflector)
Interceptor响应转换/缓存/日志Handler 前后各执行一次,基于 RxJS Observable
Pipe参数转换与校验作用于 Handler 参数级别,可抛出 BadRequestException
ExceptionFilter统一异常响应捕获上述所有层抛出的异常

作用域优先级:全局 → 控制器 → 方法。

问题六:koa-compose 的核心实现原理?为什么要检查 next() 多次调用?

koa-compose 通过递归 dispatch 函数实现。闭包维护 index 变量追踪当前执行位置,每个中间件的 next 实际是 dispatch(i+1) 的绑定调用。

检查 i <= index 防止 next() 被多次调用,否则下游中间件会被重复执行,导致:响应被重复发送、数据库写入等副作用重复执行、中间件依赖的上下文状态混乱。这个检查确保了洋葱模型的确定性和执行的幂等性。

问题七:Express 的路由匹配机制是怎样的?:id 参数的底层原理?

Express 使用 path-to-regexp 库将路由路径编译为正则表达式。/users/:id 会被编译为类似 /^\/users\/([^\/]+?)\/?$/i 的正则。请求到达时,遍历注册的路由,逐一用正则匹配请求路径,捕获组对应路由参数。

匹配遵循先注册先匹配原则,因此具体路径应放在参数路径前面(/users/me/users/:id 之前)。router.param() 可以为特定参数注册预处理中间件,避免在每个路由中重复查询逻辑。

问题八:生产环境如何设计健壮的错误处理体系?

分层设计:

应用层:try/catch + 错误中间件/ExceptionFilter
框架层:asyncHandler / koa-compose / Nest ExceptionFilter
进程层:process.on('unhandledRejection') + process.on('uncaughtException')
系统层:PM2/K8s 进程监控与自动重启

关键实践:

  • 生产环境绝不暴露 stack trace 和内部错误详情
  • 所有错误响应使用统一 JSON 结构(包含 code/message/timestamp/path)
  • 5xx 错误必须记录到日志系统(ELK / Sentry)
  • 实现请求 ID 追踪(X-Request-Id),方便日志关联
  • 区分客户端错误(4xx,返回详情帮助修正)和服务端错误(5xx,返回通用信息)
  • uncaughtException 捕获后应优雅退出,不要继续运行在不确定状态中

8. 延伸阅读

源码与原理

  • Express 源码 — 重点阅读 lib/router/index.js(Router 实现)和 lib/router/layer.js(Layer 匹配机制)
  • Koa 源码 — 核心文件仅 4 个:application.jscontext.jsrequest.jsresponse.js
  • koa-compose 源码 — 不到 50 行代码实现洋葱模型,是理解函数式编程和 Promise 链的经典案例
  • path-to-regexp — Express/Koa 路由参数解析的底层库

框架进阶

设计模式

  • 《依赖注入原理、实践与模式》— Mark Seemann,DI 和 IoC 的权威著作
  • InversifyJS — TypeScript 的 IoC 容器独立实现,理解 Nest.js DI 原理的好参考
  • reflect-metadata 提案 — 装饰器元数据反射的 TC39 提案

性能与最佳实践

用心学习,用代码说话 💻