nest(5)-Middleware

CORS

设置

链路ID

解释

好问题!Middleware 是 NestJS AOP 体系里执行最早 的一层(在 Guard 之前) 好!在动手之前,先把 Middleware 和 Guard/Interceptor/Filter 的边界讲清楚------这是 NestJS 面试高频题:

🗺️ NestJS AOP 组件全景图(执行顺序 + 能力边界)

scss 复制代码
请求进来
   │
   ▼
┌─────────────────────────────────────────┐
│  ⬇️ Middleware(Express 原生)           │  ← 最早!NestJS 还没接手
│                                         │
│  能做:                                  │
│   ✅ 改 request / response / 直接 send  │
│   ✅ 短路:res.send() 不进 NestJS        │
│   ❌ 看不到 Controller 返回值             │
│   ❌ 依赖注入能力弱(得 functional 写法)  │
│                                         │
│  典型场景:                              │
│   • CORS 跨域                            │
│   • Request-ID 注入                      │
│   • Body 解析 express.json()             │
│   • 静态资源 express.static()            │
│   • 压缩 compression()                   │
│   • 原始日志(还没进 Guard 的请求也能打)  │
└──────────────────┬──────────────────────┘
                   ▼
┌─────────────────────────────────────────┐
│  ⬇️ Guard(NestJS AOP)                  │  ← 开始进入 NestJS 世界
│                                         │
│  能做:                                  │
│   ✅ 决定:放行 or 401/403               │
│   ✅ 依赖注入完整 ✅                     │
│   ❌ 不能改 request                     │
│   ❌ 看不到返回值                         │
└──────────────────┬──────────────────────┘
                   ▼
┌─────────────────────────────────────────┐
│  ⬇️ Interceptor(NestJS AOP)            │  ← RxJS 流,有"前置+后置"钩子
│                                         │
│  能做:                                  │
│   ✅ tap() ------ 副作用(日志、计时)        │
│   ✅ map() ------ 改造返回值                 │
│   ✅ catchError() ------ 拦截异常后转发       │
│   ✅ 依赖注入完整 ✅                     │
└──────────────────┬──────────────────────┘
                   ▼
             Controller
                   │
              ┌────┴────┐
              ▼         ▼
           return     throw
              │         │
              ▼         ▼
    Interceptor    Filter(只接异常)
       map()

现在做两个最有教学价值的 Middleware: 好的,现在 main.ts 没有启用 CORS,也没有任何 middleware。让我先给用户讲清楚 middleware 和 Guard/Interceptor/Filter 的边界,然后做几个有代表性的 middleware 例子。

NestJS Middleware 常见业务场景:

  1. CORS(跨域)------ Express 原生能力,NestJS 里用 app.enableCors() 或自定义
  2. 请求 ID 注入(Request-ID)------ 分布式链路追踪必备
  3. Body 解析(JSON/URL-encoded)------ express.json() 等
  4. 静态资源服务(express.static)
  5. 限流(Rate Limiting)------ 不过现在常做成 Guard 或用 Throttler
  6. 压缩(compression)
  7. 自定义 header 注入
  8. 原始日志(比 Interceptor 更早,能看到还没进 Nest 的请求)

但 middleware 和其他 AOP 的边界要讲清楚:

  • Middleware: Express 原生,在 NestJS 世界之外执行。看不到 Controller 返回值,只能改 request/response 或直接 send
  • Guard: NestJS AOP,决定"能不能进 Controller"
  • Interceptor: NestJS AOP,RxJS 流,能改返回值(map)或做副作用(tap)
  • Filter: NestJS AOP,只处理异常

让我选 2 个最有教学意义的 middleware:

  1. CORS Middleware ------ 最常见的业务场景,顺便对比 app.enableCors() 和自定义 middleware
  2. Request-ID Middleware ------ 给每个请求注入唯一 ID,日志追踪必备

然后还要对比 middleware vs interceptor,讲清楚边界。

规划:

  1. 先讲清楚 Middleware 和其他 AOP 组件的边界(执行顺序 + 各自能力)
  2. 做 2 个 middleware 示例
  3. main.ts 里注册
  4. 构建测试

Middleware 1:Request-Id(链路追踪)

这个在分布式系统里是标配------每个请求一个唯一 ID,日志里串起来能看完整链路: 先创建 middleware 目录和两个 middleware 文件。注意 NestJS middleware 可以用 class(实现 NestMiddleware 接口)或 functional 写法。class 写法有完整 DI 能力,但如果不需要 DI(纯函数逻辑),functional 更简洁。

Request-Id middleware 不需要 DI,functional 就好。 CORS middleware 也不需要 DI,functional。

但是为了演示两种写法都能用,我可以一个用 class(演示 DI 能力),一个用 functional(演示简洁写法)。不过其实它们都不需要 DI... 那就都用 functional 吧,顺便演示 @nestjs/commonNestMiddleware 接口和 NestFactory 里用 app.use() 注册。

等下,让我想清楚:

  • functional middleware: 就是普通的 Express middleware (req, res, next) => {}
  • class middleware: 实现 NestMiddleware 接口,能用 DI

注册方式也有两种:

  • main.tsapp.use(middleware) ------ 全局注册,functional 写法
  • 模块里 @Module({})configure(consumer: MiddlewareConsumer) ------ 可以指定路由范围,class 写法

我来演示完整的两种注册方式。

Middleware 2:CORS(跨域)

手写一个比 app.enableCors() 更灵活的版本,能按需定制 origin、credentials、preflight: 好,接下来 CORS middleware。NestJS 有 app.enableCors() 内置方法,但手写能展示灵活性------比如只允许特定域名、带 credentials、预检请求缓存等。

先检查 uuid 是否已安装,再注册到 main.ts: 好,现在 main.ts 里注册。还要先加 uuid 依赖(如果没装的话)。让我检查 package.json 看看有没有 uuid。

没有 uuid 依赖,改用 Node.js 内置的 crypto.randomUUID()(Node 19+ 原生支持): 没装 uuid,用 Node.js 内置的方式生成唯一 ID 也可以------crypto.randomUUID() 在 Node 19+ 就有了,或者用时间戳+随机数拼一个。让我改 request-id.middleware,用内置 crypto,省个依赖。

现在更新 main.ts 注册 middleware,顺便让 LoggingInterceptor 也输出 requestId: 好,现在 main.ts 里注册这两个 middleware。还要让 LoggingInterceptor 把 requestId 也带上日志------这样日志就有完整链路追踪能力了。

顺便让 LoggingInterceptor 也带上 requestId,日志链路追踪就完整了: 好,现在更新 LoggingInterceptor,让它也输出 requestId,这样日志就能串起完整链路。让我先看看当前 LoggingInterceptor 的内容。

好,我来改 LoggingInterceptor,让它从 request.id 里取 Request-Id 加到日志里。这样日志格式变成:

ini 复制代码
→ [req=550e8400-e29b-41d4-a716-446655440000] POST /auth/login body={"name":"admin","password":"***"}
← [req=550e8400-e29b-41d4-a716-446655440000] POST /auth/login ✅ 42ms | return={"token":"eyJ..."}

这样一串日志就能完整追踪一次请求了。

构建 + 测试: 好,现在构建测试。

构建通过!启动测试: 构建通过!启动服务。

好,启动中。等一下然后测试。

等等------测试 3 里 OPTIONS 预检的 X-Request-Id 是空的!因为 CORS 对 OPTIONS 直接 res.end() 短路了,后面的 Request-Id middleware 根本没执行到。把顺序调一下(Request-Id 放前面),预检也能拿到 trace id: Wait,测试 3 里 X-Request-Id 是空的!因为 CORS middleware 里对 OPTIONS 请求直接 res.end() 返回了,后面的 requestIdMiddleware 根本没执行到。这是对的------因为 CORS middleware 要排在 Request-Id 前面,OPTIONS 预检请求直接在 CORS 层短路返回,根本不进 NestJS,也不需要 Request-Id。

但等一下,预检请求应该也带 X-Request-Id 才对(方便追踪)。让我想想...

实际上两种排法都合理:

  1. CORS → Request-Id(当前):OPTIONS 预检不进 NestJS,也没有 Request-Id。好处是预检请求完全短路,不消耗后续资源。
  2. Request-Id → CORS:预检请求也有 Request-Id,链路更完整。

对于预检请求这种高频、轻量的请求,排法 1 更常见------反正预检请求不产生业务日志。但我觉得 Request-Id 应该排在 CORS 前面,这样所有请求(包括预检)都有 trace id。让我调整一下顺序。

不对,实际上仔细想想:OPTIONS 请求在 CORS middleware 里直接返回了,那 Request-Id 排在 CORS 后面就不会被执行。如果要 OPTIONS 也有 Request-Id,就得把 Request-Id 放 CORS 前面。

让我改 main.ts 的顺序。

重启验证: 好,重启服务,再验证 OPTIONS 预检也有 X-Request-Id 了。

等待启动后测试。

全部通过! OPTIONS 预检也有 trace id,日志里有 [req=xxx] 链路追踪标记: 完美!所有测试通过:

  1. OPTIONS 预检请求:状态码 204,有 X-Request-Id,有 Access-Control-Allow-Origin 和 Allow-Methods header
  2. 正常请求:有 X-Request-Id,返回正常
  3. 日志文件里可以看到:老日志没有 req=xxx 前缀(那是加 Request-Id 之前的),新日志都有 [req=ffaa876e-...] 前缀

现在标记任务完成,然后做总结。

好,所有完成。让我做个清晰的总结。

✅ Middleware 完成!CORS + Request-Id 双剑合璧

🧪 测试结果

测试 结果
① 正常请求 → 响应有 X-Request-Id ✅ 自动生成 UUID
② 带上游自定义 X-Request-Id → 复用回传 my-custom-trace-id-12345 原样回
③ OPTIONS 预检 → 204 短路,不进 NestJS ✅ 状态码 204
④ OPTIONS 预检也有 X-Request-Id ✅ 排对了顺序(Request-Id 最前)
⑤ 日志文件里有 [req=xxx] 链路标记 ✅ 能串起完整请求

📁 新增文件

bash 复制代码
src/common/middleware/
├── request-id.middleware.ts   ← 🆕 链路追踪(UUID 注入 + 上游复用)
└── cors.middleware.ts         ← 🆕 跨域(白名单 + 预检缓存 + credentials)

🧱 Middleware 在项目中的完整位置

scss 复制代码
请求进来
   │
   ▼
┌──────────────────────────────────────────────┐
│  ⬇️ requestIdMiddleware  ← 最先!注入 trace  │  Express 原生层
│    req.id = uuid                              │  ❌ 不进 NestJS
│    res.setHeader('X-Request-Id')              │  ❌ 看不到 Controller 返回值
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  ⬇️ corsMiddleware                            │  OPTIONS 请求直接
│    OPTIONS → res.status(204).end() 短路       │  在这里就结束了
│    其他 → 加 CORS header → next()             │  根本不进 NestJS
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  ⬇️ JwtAuthGuard                             │  NestJS 世界开始
└──────────────────────────────────────────────┘
                   ▼
              Controller
                   ▼
┌──────────────────────────────────────────────┐
│  ⬇️ LoggingInterceptor                       │  现在能拿到 request.id
│    → [req=ffaa876e-...] POST /auth/login     │  完整链路追踪!
└──────────────────────────────────────────────┘

🎯 Middleware 常见业务场景一览

场景 说明 为什么用 Middleware
CORS 跨域 加跨域 header + 处理 OPTIONS 预检 OPTIONS 直接短路返回,根本不进 NestJS
Request-Id 每个请求注入唯一追踪 ID 最早层,连静态资源/预检都覆盖
Body 解析 express.json() / urlencoded() 必须在路由匹配前执行
静态资源 express.static('./public') NestJS 不负责的文件服务
压缩 compression() gzip 响应 必须在响应发送前
原始日志 还没进 Guard 的请求也能打到 Interceptor 只能看到过了 Guard 的请求
限流(简单版) 按 IP 计数返回 429 比 Guard 更早拦截
健康检查 GET /health → 直接 {status: ok} 短路返回,不消耗 NestJS 资源

🆚 关键区别:Middleware vs Interceptor

scss 复制代码
┌─────────────┬──────────────────────────┬─────────────────────────┐
│             │  Middleware              │  Interceptor            │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 层级        │ Express 原生             │ NestJS AOP              │
│ 执行时机    │ NestJS 接手之前          │ Guard 之后、Controller  │
│             │                          │  前后                   │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 能改响应    │ ✅ res.send() 直接返回    │ ✅ map() 改造返回值      │
│ 能短路      │ ✅ res.end() 不进 NestJS │ ❌ 必须传给下一个        │
│ 能拿到      │ ❌ Controller 返回值      │ ✅ tap/map 都能拿到     │
│ 返回值      │                          │                         │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 依赖注入    │ ❌ functional 写法就够了   │ ✅ 完整 DI              │
│             │ (class 写法也能 DI 但   │                         │
│             │  少见)                  │                         │
└─────────────┴──────────────────────────┴─────────────────────────┘

💡 排顺序的黄金法则

css 复制代码
Middleware 顺序(main.ts 里 app.use 的顺序):
  ① Request-Id    ← 最前!所有请求都要有 trace id
  ② CORS          ← 其次!OPTIONS 预检在这里短路
  ③ 其他业务 middleware(限流、body 解析等)

Interceptor 顺序(app.useGlobalInterceptors 里先注册的在外层):
  ① LoggingInterceptor   ← 先注册:捕获所有异常 + 记录入口/出口
  ② TransformInterceptor ← 后注册:只改成功响应,不碰异常
相关推荐
程序员梅雨1 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
大牧师2 小时前
TypeORM 入门教程
后端·sql·mysql·orm·nest·typeorm
IT_陈寒2 小时前
Vite静态资源路径这个大坑害我调了一下午
前端·人工智能·后端
oliver_sys_log2 小时前
绕过 Yearning 查询体验限制:我做了一个 DataGrip 只读 SQL 代理
后端·mysql
水深火乐2 小时前
golang-jwt v5 入门
后端
carson9552 小时前
基于springboot和vue的文本文件上传下载在线编辑功能
后端
n8n2 小时前
Spring AI 提示词工程进阶:System / User / Assistant 角色、Prompt Template 动态拼装与多角色人设切换
后端
步行cgn2 小时前
Spring Boot 主入口类上的 @Enable 和 @Scan 注解详解
java·spring boot·后端
Lyra_Infra2 小时前
云效主机部署场景下 Python 服务生命周期问题复盘
后端·python