NestJS 12迁移指南:ESM、标准模式与可观测性功能
本指南详细介绍了NestJS 12的核心变化——ESM包、标准模式验证、内置可观测性功能以及CLI工具的更新——以及如何安全地进行迁移。
NestJS 12 已正式发布,与典型的重大版本升级不同,此次更新并非围绕某一项核心功能展开。
相反,它同时改进了 NestJS 生态系统的多个方面,使其更符合当今后端开发的实际需求。
其中最值得关注的更新包括:
- Nest 包现在以 ESM 方式分发
- 基于标准模式实现验证功能
- 基于标准模式实现序列化功能
- 通过
@nestjs/observe提供内置的可观测性功能 - 重新设计的 NestJS CLI 工具
- 为新的单仓库项目提供 Rspack 支持
- 新项目中默认集成 Vitest 和 oxlint 工具
- 更智能的冲突路由检测功能
- 工具能够解析的错误代码
- 结构化、便于机器处理的日志记录
如果您已经在使用 NestJS 代码库,有一个细节可以立刻消除您的顾虑:
仅仅因为 NestJS 12 本身以 ESM 形式发布,并不意味着就必须将您的应用转换为 ESM。
这一事实使得原本可能显得具有颠覆性的升级变成了可以按自己节奏推进的任务。
NestJS 12 的重点在于让框架保持最新
NestJS 广泛用于在 Node.js 和 TypeScript 基础上构建结构清晰的后端服务。
它的整体架构并未改变,任何曾经使用过它的人都能立刻认出其结构:
NestJS 12 Is About Modernizing the Framework
NestJS has become one of the popular ways to build structured backend applications with Node.js and TypeScript.
Its architecture is familiar:
发生变化的是周围的 Node.js 生态环境。
整个生态系统中对 ESM 的采用率持续上升。
像 Zod 这样的 Schema 库越来越受欢迎。
更新、更快速的打包工具正在取代旧式的构建工具。
在开发过程中,可观测性越来越被视为首要考量因素,而非在服务发布后才添加的功能。
NestJS 12实际上一次性跟上了所有这些发展趋势。
不过值得注意的是,这一切并不要求现有应用从第一天起就全部采用新功能。
1. 核心Nest包现在以ESM格式发布
此次版本中最明显的变化或许是Nest的核心包现在以ESM格式发布。
如果你的项目是基于CommonJS构建的,这听起来似乎需要大规模重写。
幸运的是,现代版本的Node.js支持require(esm)。
实际上,这意味着大多数CommonJS应用可以保持原有运行方式,无需完全转换为ESM格式。
例如,以下代码行仍能像以前一样正常工作:
const { NestFactory } = require('@nestjs/core');
无需将其重写为以下形式:
import { NestFactory } from '@nestjs/core';
不过,NestJS 12确实提高了所需的Node.js最低版本要求。
具体来说,你需要使用以下版本之一:
Node.js 20.19+
or
Node.js 22.12+
明确不支持Node.js 21.x版本。
因此在修改NestJS相关依赖之前,请先确认当前使用的Node版本:
node --version
在CI/CD流程的早期阶段进行此项检查也是一种明智的预防措施。
2. 转换为ESM是可选而非强制要求
对于正在维护现有项目的团队而言,这一点尤其重要。
这里涉及两次独立的迁移工作。
NestJS本身正在将其包转换为ESM格式。
但你的应用程序并无必须立即跟进的义务。
换言之,这种配置是完全可行的:
Existing CommonJS Application
↓
NestJS 12
↓
Continue running CommonJS
而非被迫采用:
CommonJS
↓
Rewrite everything
↓
ESM
↓
NestJS 12
尽管如此,项目中的自定义工具仍可能带来问题。
值得仔细检查的内容包括:
- 自定义的 Bootstrap 脚本
- 构建流水线
- 测试运行工具
- 打包器配置
- 非标准的导入方式
- 专门为 CommonJS 设计的工具
即使 NestJS 本身运行正常,流水线中其他地方依赖的脚本或工具也可能存在问题。
3. 标准模式的支持改变了验证方式
NestJS 12 的一个重要新增功能是对标准模式的原生支持。
如果你最近使用过 TypeScript,很可能接触过 Zod、Valibot 或 ArkType 等库。这些工具能够在运行时进行验证,同时与 TypeScript 的类型检查功能完美集成。
从历史上看,NestJS 依赖基于类的 DTO 以及 class-validator。这种模式依然有效且不会被废弃,NestJS 12只是提供了另一种实现方式。
实际应用中的样子如下:
@Post()
create(
@Body({
schema: createUserSchema,
})
body: CreateUserDto,
) {
return this.usersService.create(body);
}
接着在全局范围内进行配置:
app.useGlobalPipes(
new StandardSchemaValidationPipe(),
);
通过这种方式,架构本身就能负责验证传入的请求。如果你的代码库已经使用 Zod 或其他遵循标准架构规范的库来定义架构,那就更加方便了。
4. Zod 与 NestJS 的集成更为直接
假设你已经定义了如下格式的 Zod 架构:
const createUserSchema = z.object({
name: z.string().min(1),
email: z.email(),
});
无需将相关逻辑复制到独立的 NestJS 专用验证层中,可以直接将现有架构集成到请求处理流程中。
路由参数也是如此:
@Get(':id')
findOne(
@Param('id', {
schema: z.coerce
.number()
.int()
.positive(),
})
id: number,
) {
return this.usersService.findOne(id);
}
这减少了重复逻辑。团队无需为客户端和服务器分别维护不同的验证规则集,只要环境允许,就可以共享同一个架构。此外,这些架构还能用于生成 OpenAPI 文档。
5. 标准架构也适用于输出响应
验证不仅限于传入的数据——输出数据同样重要。
假设某个接口意外返回了类似以下内容:
{
"id": 1,
"name": "John",
"passwordHash": "..."
}
从技术上讲,处理程序确实返回了一个对象。但该对象可能暴露了超出 API 合同规定范围的信息。
为了解决这个问题,NestJS 12 提供了 StandardSchemaSerializerInterceptor,它会在数据传送到客户端之前对其进行检查并重新格式化。
例如:
@UseInterceptors(
StandardSchemaSerializerInterceptor,
)
@SerializeOptions({
schema: userResponseSchema,
})
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
这样就能在请求处理的整个周期中实现全面的验证覆盖:
Client
↓
Request
↓
Schema Validation
↓
Application
↓
Schema Serialization
↓
Response
↓
Client
对于以 API 为核心构建的服务而言,这种对称性带来了实质性的改进。
6. 通过 @nestjs/observe 实现内置的可观测性
NestJS 12 还引入了一个专门用于可观测性的包:
@nestjs/observe
它的独特之处在于能够了解 NestJS 的内部结构。普通的监控代理只能看到类似这样的信息:
POST /users
200
相比之下,Nest 自带的工具能够识别出控制器、提供者、GraphQL 解析器、队列消费者、任务以及微服务等更高层次的架构元素。
该监控功能覆盖了多个领域,包括 HTTP、GraphQL、gRPC、微服务、队列消费者以及定时任务。
其目标是将可观测性视为嵌入应用程序生命周期中的要素,而非在HTTP服务器层面外部添加的组件。
7. 可观测性是QA应当关注的问题
从QA的角度来看,这一可观测性层值得重视。
API返回响应后测试工作不应立即停止:
200 OK
了解在生成该响应期间实际发生了什么具有重要价值。
设想请求在系统中按如下方式流转:
Request
↓
Controller
↓
Service
↓
Database
↓
External API
↓
Response
如果某个调用需要三秒才能完成,仅凭200状态码无法说明全部情况。
真正需要了解的是这三秒时间用在了何处。
可能的成因包括:
- 缓慢的数据库查询
- 外部API带来的延迟
- 应用程序逻辑处理所需的时间
可观测性数据为质量保障团队和工程团队提供了额外的证据依据,有助于将测试失败与实际生产环境中的行为联系起来。
8. 配置验证转向标准模式
配置处理也是正在得到更新的领域之一。
过去,许多 NestJS 应用都依赖 Joi 来处理配置:
ConfigModule.forRoot({
validationSchema: schema,
});
从 NestJS 12 开始,配置验证将转向标准模式。
实际应用中的情况如下:
ConfigModule.forRoot({
validationSchema: z.object({
NODE_ENV: z
.enum([
'development',
'production',
'test',
])
.default('development'),
PORT: z.coerce
.number()
.default(3000),
}),
});
Joi 并未被废弃——现有项目仍可继续使用它,但需要升级到 Joi 18 或更高版本,并将所有与该库相关的设置移至:
validationOptions.libraryOptions
这是整个框架生态系统朝着统一模式接口发展的整体举措的一部分。
9. 现在可自动检测冲突的路由
存在一个微妙的 API 设计缺陷,往往要等到出现问题时才容易发现。
假设你定义了以下两个处理程序:
@Get(':id')
findOne() {}
@Get('me')
getCurrentUser() {}
根据路由的解析方式以及声明顺序,针对以下请求:
/users/me
可能会匹配到这样的模式:
/users/:id
而非按预期调用专用的 /me 处理程序。
NestJS 12 添加了一项可选的诊断功能,可用于识别这类路由歧义。
启用方法如下:
const app = await NestFactory.create(
AppModule,
{
routeConflictPolicy: {
duplicate: 'error',
shadow: 'warn',
},
routeResolutionStrategy:
'specificity',
},
);
这能让开发者提前发现模糊的路由规则,而不会在之后通过令人困惑的 API 响应才偶然发现问题。
10. 计算机能够真正解析的错误代码
在这里再加一个小改动,可能会对所有使用该 API 的应用产生重大影响。
以这个异常为例:
throw new BadRequestException(
'Password is too weak',
);
前端开发者可能会倾向于直接根据错误信息文本进行匹配:
if (message === 'Password is too weak') {
...
}
这种做法很脆弱,因为错误信息的表述随时可能发生变化。
更可靠的方案是使用稳定的错误代码:
throw new BadRequestException(
'Password is too weak',
{
errorCode: 'WEAK_PASSWORD',
},
);
这样客户端就可以根据该错误代码进行判断:
WEAK_PASSWORD
而无需依赖精确的错误信息表述。
当一个 API 有多个使用者时,这一点尤为重要,例如:
- 网页前端
- 移动应用
- 面向合作伙伴的 API
- 内部服务
所有这些使用者都可以依赖相同的统一错误标识符,而无需解析人类可读的文本。
结构化日志得到升级
此版本中的日志功能也有了改进。
现在你可以这样编写代码:
logger.log(
'User created',
{
userId: 1,
email: 'foo@bar.com',
},
);
对象参数被视为附加在特定日志行上的结构化数据,而不仅仅是需要打印的额外文本。
当启用 JSON 输出模式时,这些结构化数据会以 params 键的形式出现,或者可以通过 flattenParams 选项直接整合到日志条目中。
如果你的日志会被输入到监控或可观测性系统中,这一点非常重要。应用程序可以输出结构化条目,从而从一开始就实现搜索和过滤功能,而无需先输出后续需要解析的普通字符串。
例如,一条日志条目可能如下所示:
{
"message": "User created",
"params": {
"userId": 1,
"email": "foo@bar.com"
}
}
与从纯文本消息中提取字段相比,这种格式要容易查询得多。
CLI已重新构建
此次版本的另一项重大变化是对CLI的全面重写。
其代码库已迁移到ESM格式,测试框架也从Jest更换为Vitest。CLI命令增加了端到端测试覆盖,并且内部命令结构也围绕类型化的上下文对象进行了重构。
这些改动未必会直接影响您的应用程序代码。但这表明现代化进程并不仅限于运行时本身——工具和开发者工作流程也在同步更新。
nest upgrade简化了迁移路径
重新构建后的CLI带来了一个新命令:
nest upgrade
在应用任何更改之前,您可以先查看它打算修改的内容:
Before running it, you can preview the changes:
这一点非常重要,因为版本升级通常会涉及许多细微且互不相关的配置细节。升级命令可以自动处理以下这类变更:
- 提升
@nestjs/*包的版本 - 更新 webpack 配置
- 将 GraphQL Playground 替换为 GraphiQL
- 调整 GraphQL 订阅传输方式
- 更新与 NATS 相关的包
- 调整
@nestjs/config的使用方式 - 更新 Jest 依赖项
- 更新 Joi 依赖项
升级完成后,它会输出一份总结,说明哪些内容已自动更改,哪些仍需手动检查。
新生成的项目采用现代默认设置
现在使用 NestJS 12 创建全新项目时,会拥有不同的起始点。
新的单仓库项目默认使用 Rspack 作为打包工具。新项目则采用 oxlint 替代 ESLint。基于 ESM 的项目现在默认使用 Vitest 作为测试运行器。除了现有的选项外,Bun 也被视为可用的包管理器选择:
npm
yarn
pnpm
这些变更不会追溯影响现有项目——这一点很重要。NestJS 12 只是为今后创建的新项目设定更现代化的基准,同时允许现有应用按自己的节奏进行迁移。
GraphQL 配置需要谨慎处理
如果你在运行 GraphQL 应用,有一些迁移工作是绝不能跳过的。
GraphiQL 现已取代 GraphQL Playground 成为默认的 IDE。更重要的是,以下功能:
subscriptions-transport-ws
已完全被移除。你需要转而使用:
graphql-ws
这两种协议在数据传输层上互不兼容。这意味着更改后端的订阅传输方式并非仅涉及后端本身——所有使用这些订阅服务的部分都需要进行更新和测试:
NestJS API
↓
GraphQL Subscription
↓
Web / Mobile Client
仅在服务器端更新依赖项并不足以确保整个系统的正常运行。
NATS支持也发生了变化
该框架现在用以下内容替换了nats包:
@nats-io/transport-node
如果您的应用程序直接导入了旧版本包,那么就需要同时更新该依赖项以及相应的导入语句。
数据包的处理方式也发生了变化:有效载荷现在以 JSON 字符串的形式序列化,而您编写的任何自定义反序列化器都将接收到完整的 NATS 消息对象,而非已预解析的有效载荷。您可以使用以下方式读取有效载荷内容:
msg.json()
如果消息传递是您系统的核心部分,那么在集成测试和回归测试计划中应特别提及这一变化。
17. 生命周期钩子的执行顺序已改变
与生命周期钩子相关的另一个破坏性变化是:
在 NestJS 12 中,生命周期钩子的触发顺序现在取决于组件在层级结构中的位置。
如果您的应用程序在以下阶段依赖特定的执行顺序,这一点就非常重要:
- 初始化
- 启动
- 关闭
- 清理
假设某个服务按以下顺序启动资源:
Database
Queue
Cache
External API
而另一项服务则假设这些资源中已有某个可用。升级后,你需要确认这一假设依然成立。
这类变更并不一定会表现为构建失败——你的项目可以无误地编译,但在运行时的表现却可能有所不同。
18. 其他值得了解的变更
此次发布还包含一些其他调整。
NestJS 12 还涉及以下方面:
- 验证错误响应的格式
- gRPC 异常的处理方式
- Kafka 的正则表达式匹配功能
- 基于请求范围的 WebSocket 网关
- WebSocket 断开连接时显示的原因
- 微服务的预请求钩子功能
- Express 中的优雅关闭机制
- HTTP 适配器对错误的映射方式
大多数项目不会用到这些功能中的每一个。但只要您的应用确实依赖了其中某项功能,就有必要为其添加针对性的回归测试。
19. 升级后 QA 应重点关注什么?
这可以说是最需要解答的重要问题。
仅通过运行以下操作来确认框架升级是否顺利显然是不够的:
npm test
相反,测试应划分为不同的领域进行。
API
需检查:
- 身份验证
- 权限控制
- 数据校验
- 错误响应
- 路由匹配
- 响应序列化
配置
需检查:
- 必需的环境变量
- 无效值
- 默认值
- 生产环境配置
- 测试环境配置
GraphQL
如相关的话:
- 查询
- 变更操作
- 订阅功能
- GraphiQL
- 客户端兼容性
微服务
如相关,则包括:
- NATS
- Kafka
- gRPC
- 消息序列化
- 重试机制
- 异常处理
可观测性
若已启用,则包括:
- HTTP追踪
- GraphQL追踪
- 后台任务
- 队列消费者
- 错误处理
- cron任务
关闭流程
需检查:
- SIGTERM信号处理
- 正在处理的请求
- 数据库连接
- 队列状态
- 后台工作进程
其目的并非回答:
"应用能否启动?"
而是要回答:
"在所有关键场景下,应用仍能正常运行吗?"
20. 此次发布对质量保障工作的意义
有一个值得关注的更宏观趋势。
框架正变得越来越自动化,验证流程逐渐标准化,可观测性功能被直接内置,日志默认以结构化形式生成,路由冲突也能自动检测,测试工具的速度也在不断提升。
这些变化并未消除对质量保障的需求,只是改变了质量保障能创造最大价值的领域。
不应只询问:
"这个接口能正常工作吗?"
质量保障人员需要越来越多地询问:
“API契约仍然正确吗?” “故障是否可以被检测到?” “权限控制是否得到妥善执行?” “错误信息是否便于机器读取?” “序列化处理是否正确?” “应用程序能否按预期恢复?” “此次升级是否会改变现有行为?”
该框架可以自动执行某些检查,但首先仍需由人工决定哪些内容需要检查。
21. NestJS 12升级的推荐路径
不要直接对生产环境进行升级,应先检查当前环境:
node --version
确认你使用的是:
Node 20.19+
或者:
Node 22.12+
接下来,更新CLI工具:
npm i -g @nestjs/cli@latest
预览此次升级将会产生的影响:
nest upgrade --dry-run
仔细查看输出结果,然后将其应用到实际中:
nest upgrade
之后,运行以下命令:
npm test
同时还要运行集成测试和端到端测试套件。请特别关注以下功能模块:
- GraphQL
- NATS
- 配置验证
- 自定义处理管道
- 生命周期钩子
- Webpack
- 基于CommonJS的工具集
每个领域都存在需要单独检查的迁移相关问题。
总结
NestJS 12并非仅仅是新增了某个功能而已。
它代表着NestJS向Node.js和TypeScript生态系统当前发展方向靠拢的一步。
ESM现已融入到包架构之中。
标准Schema使得该框架能够使用Zod、Valibot、ArkType等验证库。
同样的架构生态系统也可用于序列化处理。
可观测性现在与 Nest 自身的应用结构有了更直接的关联。
CLI 已根据新的工具进行了重构。
Rspack、Vitest、oxlint 和 Bun 正逐渐成为现代 NestJS 环境的组成部分。
同时,这些变化并不会迫使现有应用立即进行全面改造。
你可以继续使用 CommonJS。
也可以继续依赖基于类的验证方式。
无需立即切换到 Vitest 或 oxlint。
这种灵活性可以说是此次版本最实用的特点。
NestJS 12 在无需所有现有应用同时现代化的前提下,让该框架保持最新状态。
对开发者而言,这意味着他们有更多空间按照自己的节奏进行改进。
对于质量保障工程师而言,这意味着又一场需要验证的重大框架升级——不仅要检查代码层面,还要涉及 API、集成功能、可观测性、配置以及实际生产环境中的表现。
正因如此,框架升级才显得如此有趣。
在 package.json 中修改版本号只是最简单的一步。
真正重要的是,应用程序能否持续保持那些依赖它的人所期望的性能表现。
相关阅读
- 在 React 前端和 Node 后端之间共享同一个 Zod 模式 — 了解如何通过单一的 Zod 模式来验证 React 表单、API 响应、Express 请求体以及环境变量,同时生成对应的 TypeScript 类型。