主题
Node.js Web 框架
目录
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 或 M2app.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 → ErrorHandler2javascript
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)
}
}
}
}逐行拆解核心逻辑:
index = -1:记录上一次执行的中间件索引,防止next()被多次调用dispatch(0):从第一个中间件开始执行i <= index检查:如果当前索引 ≤ 上次记录的索引,说明next()被重复调用fn(context, dispatch.bind(null, i + 1)):将dispatch(i+1)作为next传给当前中间件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 原生的 request 和 response 封装到一个 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 容器工作流程:
- 扫描
@Module声明的providers - 通过
reflect-metadata分析每个 provider 构造函数的参数类型 - 递归解析依赖链
- 按依赖顺序实例化
- 注入到需要的地方
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 │
└──────────────────┴──────────────────────┴──────────────────────┘共同的设计哲学:
- 控制反转(IoC):应用程序不主动创建依赖,由容器负责管理对象生命周期
- 面向切面编程(AOP):通过拦截器/守卫实现横切关注点(日志、鉴权、缓存)的解耦
- 装饰器/注解驱动:通过声明式元数据标注来定义行为,减少样板代码
- 模块化封装:功能按领域模型划分为独立模块,模块间通过导入导出通信
关键差异:
- 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 ◀────────┘
│
▼
onResponse4.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' }))内部工作原理:监听 req 的 data 事件收集 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.body 为 undefined,因为 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() 的返回值和执行语义:
- Express:
next()是普通函数调用,控制权单向传递,没有"回程"。next()之后的代码会立即执行,不会等待下游中间件完成 - Koa:
next()返回 Promise,通过await next()等待下游中间件全部执行完后再执行后续逻辑
这使得 Koa 天然适合实现耗时统计、统一响应包装、错误捕获等横切关注点。Express 实现类似功能需要借助 res.on('finish') 事件监听。
追问思路:koa-compose 的实现原理、为什么要检测 next() 多次调用、Express 5.x 在这方面有什么改进。
问题三:Nest.js 的依赖注入是如何工作的?
Nest.js 的 DI 基于 TypeScript 装饰器和 reflect-metadata:
@Injectable()装饰类时,TypeScript 编译器(需开启emitDecoratorMetadata)将构造函数参数类型信息以元数据形式保存@Module的providers声明注册可注入的 Provider- IoC 容器启动时,通过
Reflect.getMetadata('design:paramtypes', target)读取构造函数参数类型 - 递归解析依赖链,按依赖顺序实例化
- 默认单例作用域,一次实例化后全局复用
追问思路:如何实现循环依赖(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.js、context.js、request.js、response.js - koa-compose 源码 — 不到 50 行代码实现洋葱模型,是理解函数式编程和 Promise 链的经典案例
- path-to-regexp — Express/Koa 路由参数解析的底层库
框架进阶
- Nest.js 官方文档 — 特别推荐 Fundamentals 章节(Custom Providers / Async Providers / Injection Scopes)
- Fastify 官方文档 — 关注 Encapsulation / Plugins / Lifecycle 章节
- Nest.js 底层原理揭秘 — 核心包
@nestjs/core中的injector.ts、scanner.ts、router-explorer.ts
设计模式
- 《依赖注入原理、实践与模式》— Mark Seemann,DI 和 IoC 的权威著作
- InversifyJS — TypeScript 的 IoC 容器独立实现,理解 Nest.js DI 原理的好参考
- reflect-metadata 提案 — 装饰器元数据反射的 TC39 提案
性能与最佳实践
- Node.js Best Practices — Node.js 最佳实践大全(Star 90k+)
- Fastify Benchmarks — 主流 Node.js 框架性能基准测试
- The Cost of JavaScript Frameworks — 框架性能开销分析