NestJS 11 + Fastify 升级避坑指南:版本冲突、测试与 Swagger

4 阅读3分钟

遇到了个task,需要将项目的依赖 NestJS 从v10 升级到 v11 ,将 Fastify 从v4升级到v5。

这是一个破坏性的大版本更新,会导致很多周边的生态插件(如文件上传、Swagger、测试工具)都需要同步调整。

以下是本次升级遇到的问题以及解决方案。

1. 核心冲突:@fastify/multipart 的版本地狱

问题现象

项目启动失败,控制台报错提示 Fastify 版本不匹配:

fastify-plugin: expected '4.x', but found '5.0.0'

原因分析

  • NestJS 11 依赖 Fastify v5
  • 你当前的 @fastify/multipart v8.0.0 是为 Fastify v4 设计的。
  • 两者互不兼容,导致应用无法初始化。

❌ 错误的解决方法

不要试图在 package.json 中使用 resolutionsoverrides 强制锁定 Fastify 版本:

// ⚠️ 危险操作:不要这样做
"resolutions": {
  "fastify": "^5.7.1"
}

这样做虽然能绕过安装报错,但会在运行时引发不可预知的 API 调用错误(Undefined function),因为旧插件的代码逻辑并不适配新版 Fastify。

✅ 正确的解决方案

必须升级插件到适配 v5 的版本:

npm uninstall @fastify/multipart
npm install @fastify/multipart@^9.0.0

版本对应法则:

  • NestJS 10 + Fastify v4 -> @fastify/multipart v8.x
  • NestJS 11 + Fastify v5 -> @fastify/multipart v9.x

2. E2E 测试:修复 Jest 无法正常关闭 (Hang)

问题现象

升级版本后,E2E 测试虽然能跑通逻辑,但在所有用例结束后,Jest 迟迟不退出,提示 Open Handles

原因分析

这通常不是 Multipart 插件的问题,而是 Redis (ioredis)Prisma 在测试结束时未彻底断开连接。NestJS 的 app.close() 触发了模块销毁,但 ioredis 在 Cluster 模式下有时非常顽固,依然保持 TCP 连接。

解决方案

afterAll 钩子中,除了调用 app.close(),还需要显式强制断开 Redis 连接

import { Cluster } from 'ioredis';

afterAll(async () => {
  const redisClient = app.get<RedisCache>(CACHE_MANAGER).store.client as Cluster;

  // 1. 执行你的清理逻辑
  await Promise.all(
    redisClient.nodes('master').map((node) => node.flushall()),
  );

  // 2. 关闭 NestJS 应用
  await app.close();

  // 3. [关键] 强制关闭 Redis 连接,防止 Jest 挂起
  if (redisClient.status === 'ready') {
    await redisClient.quit();
  }
});

3. Swagger 升级:@ApiResponse 的类型变动

问题现象

升级 @nestjs/swagger 到 v8+ 后,代码中原本正常的装饰器突然爆红,提示 status 属性不可用。

根本原因

NestJS 团队为了规范代码语义,在 TypeScript 类型定义中做了严格区分:专用装饰器不再允许包含 status 属性,因为它们的名字已经代表了状态码。

修复对照表

场景装饰器类型是否允许 status代码示例
通用定义@ApiResponse必须@ApiResponse({ status: 201, description: '...' })
成功 (200)@ApiOkResponse禁止@ApiOkResponse({ description: '...' })
创建 (201)@ApiCreatedResponse禁止@ApiCreatedResponse({ description: '...' })
错误 (400)@ApiBadRequestResponse禁止@ApiBadRequestResponse({ description: '...' })

行动指南: 检查你的 Controller,如果使用了快捷装饰器(如 ApiOkResponse),请直接删除里面的 status: 200 属性。


总结:升级检查清单 (Checklist)

  1. 依赖清理:移除 package.json 中的 resolutions 锁定。
  2. 核心插件:将 @fastify/multipart 升级至 ^9.0.0
  3. Swagger 修正:删除所有专用装饰器(@ApiOkResponse 等)中的 status 字段。
  4. 测试稳健性:在 E2E 测试的 afterAll 中加入 redisClient.quit() 以确保测试进程优雅退出。