我创有一个 AI 交流群,如果你想学习 Agent 项目落地或者想交流 Agent 技术的的可以加我v
yunmz777,
截至 2026 年 9 月 2 日,@nestjs/core 在 npm 的 latest 已经是 12.0.1。12.0.0 是 v12 这条大版本线的首个正式版本,讨论 NestJS 12 的升级重点,核心仍然是这一轮 Major Release 带来的工程变化,而不是 12.0.0 和 12.0.1 之间的小版本差异。
如果还把这次升级理解成多了几个 Decorator,会错过真正换挡的地方。核心包开始全面转向 ESM,Standard Schema 进入请求验证、响应序列化和配置校验,CLI 开始承担自动升级,Rspack、Vitest、oxlint 成为新项目的重要默认方向,路由冲突、结构化日志、机器可读错误码和官方可观测性也一起补上。
项目如果还停在 NestJS 11,我更建议把 v12 看成一次工程基础设施升级,而不是一次普通依赖升级。
NestJS 12 真正改了什么
模块系统、验证层、CLI、构建工具和生产诊断都动了,对现有项目的影响也不一样。
| 领域 | NestJS 12 的主要变化 | 对现有项目的影响 |
|---|---|---|
| 模块系统 | 核心包转向 ESM 发布 | CommonJS 应用仍可运行,但 Node.js 需要满足 require(esm) |
| 数据验证 | 原生支持 Standard Schema | Zod、Valibot、ArkType 可直接进入验证体系 |
| 响应序列化 | 新增 StandardSchemaSerializerInterceptor |
请求和响应可以共用 Schema-first 思路 |
| 配置校验 | @nestjs/config 接入 Standard Schema |
新项目不必围着 Joi 设计,旧 Joi 项目仍可用 |
| 可观测性 | 新增官方 @nestjs/observe |
可观测 Controller、Resolver、Queue Consumer、Cron 等生命周期 |
| CLI | 新增 nest upgrade、nest deploy |
Major Upgrade 能自动处理一部分机械迁移 |
| 构建工具 | CLI 进一步转向 Rspack | Webpack 专属参数进入弃用阶段 |
| 新项目模板 | ESM 默认 Vitest,生成项目默认 oxlint | 不要求老项目同步迁测试和 Lint |
| 路由 | 增加冲突诊断和 Specificity 排序 | Express 项目可提前发现动态路由遮挡静态路由 |
| 异常 | HttpExceptionOptions 支持 errorCode |
客户端可依赖稳定错误码,不必解析文案 |
| 日志 | 普通对象参数默认成为 Structured Params | 现有日志采集、告警和 Dashboard 可能受影响 |
| GraphQL | GraphiQL 成为新默认,旧订阅协议被移除 | 使用 Subscription 的项目要检查客户端协议 |
| NATS | Transporter 切换到 NATS v3 | Driver、Payload 和自定义 Serializer、Deserializer 需要回归 |
NestJS 迁移指南 把这条主线写得很清楚,框架开始减少对某一种具体实现的强绑定,同时把更多过去需要应用自己补的工程能力放进框架正式边界。
核心包转向 ESM,但项目不用被迫一起迁移
NestJS 12 最基础的变化,是核心 Package 开始以 ESM 形式发布。当前 @nestjs/core 的 package.json 已经声明 type 为 module。
这里最容易产生一个误解,NestJS Package 变成 ESM,不代表现有应用必须立刻从 CommonJS 迁过去。较新的 Node.js 已经能在 CommonJS 里通过 require(esm) 加载符合条件的 ESM Package,所以从 NestJS 11 升到 12 时,完全可以先保留原来的 module: commonjs。nest upgrade 也不会擅自把项目改成 ESM,官方迁移指南明确把应用自身的 ESM Migration 视为可选步骤。
这个取舍很关键。如果一次 Major Upgrade 同时强迫项目完成 Node.js 升级、CommonJS 迁 ESM、Jest 迁 Vitest、Webpack 迁 Rspack、DTO 迁 Zod,风险会成倍增加。v12 更合理的地方在于,框架自己的发布格式已经切换,现有应用仍可以按自己的节奏处理工程栈。
Node.js 版本不能只看 @nestjs/core 的 engines
NestJS 12 的 Node.js 要求需要分两个场景。
| 场景 | 官方迁移指南给出的版本要求 |
|---|---|
| 运行 NestJS 12 应用 | Node.js 20.19+,或者 22.x 中至少 22.12+ |
使用 nest new、nest generate、nest upgrade 等 Schematics |
Node.js 22.22.3+、24.15+ 或 26+ |
运行时推荐门槛高于 @nestjs/core 单纯写出的 >=20,主要是因为 CommonJS 应用加载 v12 的 ESM Package 依赖 Node.js 的 require(esm) 能力。CLI Schematics 又因为底层 Angular Devkit 拥有更高的 Runtime Floor。
生产项目里,我不建议为了少升一个 Node Minor Version 去卡官方最低线。更稳妥的方式是直接统一到当前合适的 Active LTS,并让开发环境、CI、Docker 和生产环境保持一致。
如果应用自己也要迁移 ESM
真正把应用切到 ESM 时,才需要在 package.json 里加上 type: "module"。TypeScript 配置通常也要切到 NodeNext:
json
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"target": "ES2023"
}
}
新的 ESM Starter 本身也是按 NodeNext 和 ES2023 这一方向配置的。
随后最明显的代码变化是本地相对路径 Import。原来可以写 import { AppModule } from "./app.module",切到 Node ESM 后通常需要写成 import { AppModule } from "./app.module.js"。即使源文件是 app.module.ts,这里仍然写 .js,因为 Node 最终解析的是编译产物。
原来依赖 __dirname、__filename 的地方同样需要检查,例如改成 join(import.meta.dirname, "hero/hero.proto")。
所以真正容易被 ESM 影响的,通常不是 Controller、Provider 和 Module,而是工程边缘,包括自定义 Bootstrap Script、动态 require()、gRPC Proto、GraphQL Schema、静态资源路径、Jest 与 ts-jest 和自定义测试配置、自定义 Builder、Webpack、SWC、ts-node,以及深度导入某个 Package 内部文件的代码。
现有项目如果没有明确的 ESM 需求,完全可以把框架升级和应用 ESM Migration 拆成两次独立改造。
Standard Schema 是 NestJS 12 最值得关注的应用层变化
NestJS 长期以来非常典型的数据验证方式,是 Class DTO 配合 class-validator:
typescript
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
name: string;
}
然后由 ValidationPipe 在运行时执行验证。这套模式没有消失,NestJS 12 真正增加的是另一条正式路线,也就是 Standard Schema。
Standard Schema 不是一个新的 Validation Library,而是一套库之间可以共同实现的标准接口。NestJS Validation 文档 说明,Zod、Valibot、ArkType 等支持该规范的库可以被框架通过统一协议消费,而不需要分别维护一套 Zod Adapter、Valibot Adapter、ArkType Adapter。
这对 TypeScript 项目很有价值,因为验证体系开始从框架专属 DTO 向更通用的 Schema Contract 扩展。
请求参数可以直接绑定 Schema
以 Zod 为例,先定义 Schema 和推导类型:
typescript
import { z } from "zod";
export const createUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.email(),
age: z.coerce.number().int().min(18),
});
export type CreateUserDto = z.infer<typeof createUserSchema>;
Controller 可以直接在 @Body() 上提供 Schema:
typescript
import { Body, Controller, Post } from "@nestjs/common";
@Controller("users")
export class UsersController {
@Post()
create(
@Body({
schema: createUserSchema,
})
body: CreateUserDto,
) {
return body;
}
}
应用再注册 StandardSchemaValidationPipe:
typescript
import { StandardSchemaValidationPipe } from "@nestjs/common";
app.useGlobalPipes(new StandardSchemaValidationPipe());
这里要特别区分两件事。@Body({ schema }) 负责把 Schema 放进 Route Metadata,真正负责执行验证、转换以及错误处理的是 StandardSchemaValidationPipe。@Query()、@Param() 等入口也可以使用相同思路。
Path Parameter 原本一定来自字符串,现在可以直接在参数上做运行时转换:
typescript
@Get(":id")
findOne(
@Param("id", {
schema: z.coerce.number().int().positive(),
})
id: number,
) {
return this.usersService.findOne(id);
}
这样 Controller 接收到的 id 已经是经过运行时验证和转换的数字,而不必继续在业务层自己 Number(id)。
响应也可以使用 Standard Schema
v12 同时新增了 StandardSchemaSerializerInterceptor。Schema 不只能保护输入,也可以控制应用最终暴露出去的响应结构。
typescript
import { z } from "zod";
export const userResponseSchema = z.object({
id: z.string(),
name: z.string(),
email: z.email(),
});
Controller 通过 SerializeOptions 把 Response Schema 绑上去:
typescript
import {
Controller,
Get,
Param,
SerializeOptions,
StandardSchemaSerializerInterceptor,
UseInterceptors,
} from "@nestjs/common";
@Controller("users")
export class UsersController {
@Get(":id")
@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({
schema: userResponseSchema,
})
findOne(@Param("id") id: string) {
return this.usersService.findOne(id);
}
}
即使 Service 返回的数据中存在 passwordHash、内部状态或其他字段,最终对外响应也可以由 Response Schema 明确约束。这比单纯依赖 TypeScript Interface 更可靠,因为类型只存在于编译阶段。代码运行以后,来自数据库、第三方 API、Message Queue 或其他 Service 的值,不会因为写了一个 Type 就自动满足那个结构。Schema 才是真正存在于运行时的数据边界。
class-validator 没有退出历史舞台
Standard Schema 进入 NestJS,并不代表传统 Class DTO 被废弃。两条路线都有非常明确的适用边界。
| 方案 | 更适合什么项目 |
|---|---|
ValidationPipe + class-validator |
大量依赖 Class DTO、Decorator 和既有 NestJS 代码的项目 |
StandardSchemaValidationPipe |
已经大量使用 Zod、Valibot、ArkType,或需要跨端共享 Schema 的项目 |
ClassSerializerInterceptor |
Response Model 仍然基于 Class 和 class-transformer |
StandardSchemaSerializerInterceptor |
希望由 Schema 直接控制最终输出 Contract |
官方明确保留了 ValidationPipe 和 ClassSerializerInterceptor。v12 做的是扩大选择,而不是宣布 Class-based DTO 过时。
所以一个大型 NestJS 11 项目,没有必要为了升级 v12 一次性重写几百个 DTO。更实际的做法是老模块继续使用 class-validator,新模块开始试用 Standard Schema,前后端共享类型比较多的领域优先迁移,第三方输入、任务 Payload、配置文件等需要强运行时 Contract 的位置优先使用 Schema。这样才能把 Framework Upgrade 和 Domain Rewrite 分开。
配置校验终于不再和 Joi 深度绑定
Standard Schema 的另一个实际变化发生在 @nestjs/config。过去 validationSchema 几乎天然让人想到 Joi,v12 开始可以直接传入 Standard Schema Compatible Schema,例如 Zod:
typescript
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { z } from "zod";
const environmentSchema = z.object({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().min(1),
});
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: environmentSchema,
}),
],
})
export class AppModule {}
如果一个项目本来就已经大量使用 Zod,这个变化会明显减少 Validation Stack 的碎片化。请求参数、环境变量、Queue Payload、内部配置和 Tool Input 可以逐渐采用同一类 Schema Contract。
Joi 也没有被删掉,但继续使用时需要升级到支持 Standard Schema 的 Joi 18+,原来一些 Joi-specific validationOptions 需要移动到 libraryOptions。
typescript
ConfigModule.forRoot({
validationSchema: joiSchema,
validationOptions: {
libraryOptions: {
allowUnknown: false,
abortEarly: true,
},
},
});
这类变化比增加一个新 Pipe 更重要,因为它说明 NestJS 开始主动降低 Validation Layer 与某个具体 Library 的耦合。
@nestjs/observe 把 NestJS 生命周期本身纳入可观测性
NestJS 12 另一个很有工程价值的变化,是官方推出 @nestjs/observe 这一套可观测性能力。它和传统 Generic Node.js APM 的差异,在于它能进入 NestJS 自己的 Framework Lifecycle。
Generic APM 很容易观测 HTTP、数据库和网络请求,却不一定天然知道一次执行现在经过的是 Guard、Interceptor、Controller、Resolver 还是 Queue Consumer。@nestjs/observe 则能理解更多 NestJS 语义,包括 Controller、Interceptor、GraphQL Resolver、Queue Consumer、Background Job 等执行边界。NestJS Observe SDK 会把请求、任务、错误、日志和 Trace 发送到 NestJS Observe。
基础接入方式类似:
typescript
import { Module } from "@nestjs/common";
import { createObserveModule } from "@nestjs/observe";
export const { ObserveModule, ObserveInstrument } = createObserveModule();
@Module({
imports: [
ObserveModule.forRoot({
serviceId: "orders-api",
}),
],
})
export class AppModule {}
Bootstrap 时把 ObserveInstrument 传给 NestFactory.create:
typescript
const app = await NestFactory.create(AppModule, {
instrument: ObserveInstrument,
});
这并不是说所有 NestJS 12 项目都应该立刻从 OpenTelemetry、Grafana、Sentry、Datadog 或现有 APM 迁过去。如果生产环境已经有成熟 Observability Stack,我更建议把 @nestjs/observe 当成一个并行评估项,重点比较 NestJS Framework 级调用链是否更完整,Queue、Cron、Resolver 的 Trace 是否明显更好,和现有 OpenTelemetry Trace 是否重复,Logs、Metrics、Trace 是否会重复采集,数据是否必须发到 Hosted Platform,以及保留周期、成本和平台绑定是否可接受。
框架提供官方方案是一回事,是否替换生产现有基础设施是另一回事。
CLI 开始承担真正的 Major Upgrade 工作
NestJS 12 的 CLI 变化,不应该只看成又增加几个 Command。最值得关注的是 nest upgrade。
过去大型 NestJS 项目升级时,很多工作都要手动完成,包括升级多个 @nestjs/* Package、检查 GraphQL、替换 NATS Driver、处理 Webpack、调整 Config Validation,再自己阅读 Migration Guide 找遗漏。v12 开始把大量机械迁移收进 CLI。迁移指南写明,nest upgrade 会统一推进 Nest Package 版本,同时处理一部分已知迁移并输出剩余问题报告。
项目使用本地 CLI 时可以先升级 @nestjs/cli 和 @nestjs/schematics,再执行 Dry Run:
bash
pnpm add -D \
@nestjs/cli@latest \
@nestjs/schematics@latest
pnpm exec nest upgrade --dry-run
确认报告以后再执行 pnpm exec nest upgrade。它可以自动覆盖一部分典型迁移,例如对齐已知 @nestjs/* Package 的 Major Version、调整 nest-cli.json、处理部分 Webpack 到 Rspack 的配置、修改 GraphQL Playground 相关配置、更新旧 GraphQL Subscription Transport、替换 NATS Driver、调整 @nestjs/config Validation Options,并输出无法安全自动修改的行为变化。
但自动迁移有一个明确边界,它只适合机械变化,不适合替我们判断业务语义。它不会自动决定项目是否应该改成 ESM,Jest 是否值得迁到 Vitest,ESLint 是否应该改成 oxlint,某个 Lifecycle Hook 顺序变化是否会破坏业务,一个复杂 Webpack Plugin 应该如何迁到 Rspack,自定义 Serializer 改完以后消息协议是否仍兼容。
自动改代码和证明系统行为正确,是两件不同的事情。
新项目的默认工程栈也在变化
nest new 现在会让开发者选择 CommonJS 或 ESM Project。官方当前默认方向是 ESM Starter 使用 Vitest,新生成项目使用 oxlint,ESM 项目使用 module: nodenext 和 moduleResolution: nodenext,Target 提高到 ES2023。
NestJS First steps 也说明 @nestjs/testing 仍然保持 Test Runner Agnostic,所以已有 Jest 项目不需要因为框架升级立刻迁移。
NestJS 12 正在明确未来默认方向,但没有把未来默认值变成老项目的强制迁移项。
Rspack 进一步取代 Webpack 的特殊地位
CLI v12 进一步减少了对 Webpack 的特殊绑定。原来的 nest build --webpack 和 nest build --webpackPath webpack.config.js 已经进入弃用方向,新的 CLI 更强调 nest build --builder rspack。如果需要自定义 Rspack Config,可以通过相应 rspackPath 配置传入。
在 Monorepo 场景中,Rspack 也成为默认 Bundler 方向,CLI 同时增加了 Parallel Build 等能力。
现有项目如果一直使用 tsc 或 SWC,而且没有 Bundling 需求,就不需要为了 NestJS 12 强行引入 Rspack。如果项目存在复杂 Webpack Pipeline,也不应该看到 Deprecated 就直接删掉全部 Config,而应该先确认 Alias、External、Asset Copy、Source Map、Define Plugin、自定义 Loader、Native Module 和 Monorepo Package Resolution 在新 Builder 下是否保持一致。
nest deploy 不是通用云部署抽象
CLI 还增加了 nest deploy,但这个命令不要理解成 NestJS 官方终于提供了一个通吃 Docker、Kubernetes、AWS、阿里云和私有云的部署层。它实际连接的是 NestJS 官方 Mau Platform。
不使用 Mau 的项目可以完全忽略这个 Command。官方也写明 nest deploy 会把部署工作交给 Mau。这类功能属于生态扩展,不应该因为出现在 CLI 里就被当成 NestJS 12 Migration 的必选项。
路由冲突终于可以在启动阶段暴露
这个变化很实用,解决的是一个长期存在的真实问题。考虑下面的 Controller:
typescript
@Controller("users")
export class UsersController {
@Get(":id")
findOne() {}
@Get("me")
findMe() {}
}
在对注册顺序敏感的 Express 中,/users/me 可能先被 :id 捕获。应用能够正常启动,TypeScript 也完全不会报错,真正的问题直到请求进入错误 Handler 才暴露。
NestJS 12 新增了启动阶段的路由策略配置:
typescript
const app = await NestFactory.create(AppModule, {
routeConflictPolicy: {
duplicate: "error",
shadow: "warn",
},
routeResolutionStrategy: "specificity",
});
routeConflictPolicy 可以在 Bootstrap 阶段检测两类问题。duplicate 表示 Method、Path、Host 和 Version 都完全相同,shadow 表示不同 Route Pattern 可能匹配同一个请求。NestJS Controllers 文档 把 routeResolutionStrategy: "specificity" 写成更具体的 Route 优先注册,例如静态 Route 优先于参数 Route,再优先于 Wildcard。
这个能力默认不会改变旧项目行为,需要主动开启。
Fastify 项目不要照搬 Express 的判断
这里还有一个很容易被忽略的 Adapter 差异。Fastify 底层 find-my-way 本来就会按 Route Specificity 处理匹配,因此 shadow Policy 在 Fastify 上基本没有实际效果,routeResolutionStrategy: "specificity" 也不会改变 Fastify 原本行为。duplicate Detection 对 Express、Fastify 都有意义。
所以 NestJS + Fastify 项目至少可以考虑只开 routeConflictPolicy: { duplicate: "error" },没必要因为看到新 API 就照搬 Express 的完整 Route Policy。
errorCode 让异常真正成为接口契约
过去很多前端会写出 if (error.message === "Password is too weak") 这种逻辑。这非常脆弱,后端只要修改一句 Error Message,或者加入多语言,客户端判断就可能失效。
NestJS 12 的 HttpExceptionOptions 新增 errorCode:
typescript
throw new BadRequestException("密码强度不足", {
errorCode: "WEAK_PASSWORD",
});
客户端就可以围绕稳定 Code 处理:
typescript
switch (error.errorCode) {
case "WEAK_PASSWORD":
showPasswordStrengthHelp();
break;
case "EMAIL_ALREADY_EXISTS":
showEmailExistsMessage();
break;
}
这样 Message 和 Code 的职责终于可以分开,message 给人看,可以国际化、补充上下文,errorCode 给程序判断,应该稳定、可枚举、可监控。这是一个看似很小、但非常值得生产项目采用的改动。
对于大型 API,我甚至更建议围绕它继续建立统一 Error Contract,而不是让每个 Controller 随手写自己的 Code。
ConsoleLogger 的变化可能悄悄影响生产日志
NestJS 12 调整了 logger.log("User created", { userId: 1, email: "foo@example.com" }) 这类写法。现在后面的 Plain Object 会作为这条日志的 Structured Params,而不是继续被当成另一条普通 Log Message。JSON Mode 下默认会进入 params:
json
{
"message": "User created",
"params": {
"userId": 1
}
}
也可以通过 flattenParams 把字段展开到 Root。如果确实需要恢复旧行为,NestJS Logger 文档 给出了关闭结构化参数的写法:
typescript
new ConsoleLogger({
structuredParams: false,
});
这个 Breaking Change 很容易被低估。应用本身可能完全运行正常,但如果生产日志一直被 Elasticsearch、Loki、Fluent Bit、Vector 或 Logstash 按固定 JSON Path 解析,升级以后就可能出现 Dashboard 字段突然为空、Alert Query 不再命中、日志脱敏规则漏掉字段、CorrelationId Path 改变、Metrics Extraction 失效。框架成功 Build,并不代表 Logging Pipeline 已经完成迁移。
GraphQL 项目要重点检查 IDE 和 Subscription
NestJS 12 对应的新 GraphQL 版本继续推进 GraphiQL。GraphQL 快速开始 里,当前 @nestjs/graphql v14 已经把 GraphiQL 作为 GraphQL IDE,旧 GraphQL Playground 被移出正式主线。
原来类似 GraphQLModule.forRoot({ playground: true }) 的配置,应该逐步迁向:
typescript
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
graphiql: true,
});
真正风险更高的是 Subscription。subscriptions-transport-ws 已经被移除,推荐使用 graphql-ws:
typescript
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
subscriptions: {
"graphql-ws": true,
},
});
这两个协议不是 Wire-compatible,所以不能只修改 Server。升级后还要一起检查 Web、Electron、Mobile GraphQL Client,以及 WebSocket Endpoint、Authentication、connectionParams、onConnect、Reconnect、Keepalive、Gateway 和 Reverse Proxy。GraphQL Subscriptions 也提醒,graphql-ws 的 onConnect Context 结构与旧协议不同。
对于有 Subscription 的项目,这属于必须做 E2E Regression 的部分。
NATS v3 不是简单换一个 Package Name
NestJS 12 的 NATS Transporter 已经切换到 NATS v3 Driver,需要把 nats 换成 @nats-io/transport-node:
bash
pnpm remove nats
pnpm add @nats-io/transport-node
NATS 文档 明确 v12 使用 @nats-io/transport-node。
如果项目只使用简单 @MessagePattern(),改动可能还比较有限。真正需要小心的是自定义 NATS Infrastructure,例如 Header、Custom Serializer、Custom Deserializer、Codec、Message Metadata,以及手动操作 NATS Client。
这里不能只验证 TypeScript 能不能编译。任何 Message Transporter Major Upgrade,最终都应该验证两端实际交换的数据 Contract 是否仍然完全一致。所以 Request-response、Event、Header、Serializer、Deserializer 最好都做一次真实跨服务测试。
Lifecycle Hook 顺序变化要检查隐藏依赖
NestJS 12 会按组件层级处理 Lifecycle Hook,这可能改变多个关联 Provider、Module 之间 Hook 的相对顺序。官方特别要求检查 onModuleInit、onApplicationBootstrap 和 Shutdown Hook 等依赖顺序。
例如一个 Consumer 在 onModuleInit() 中立即开始消费:
typescript
@Injectable()
export class ConsumerService implements OnModuleInit {
constructor(private readonly connection: ConnectionService) {}
async onModuleInit() {
await this.connection.consume();
}
}
与此同时,ConnectionService 又依赖自己的 Lifecycle Hook 建立连接。这种设计真正的问题,并不是 NestJS 12 把 Hook 顺序改了,而是业务正确性一直依赖一个没有显式表达的初始化顺序。
更可靠的方式是通过 Explicit Ready State、Promise、Readiness Barrier、Bootstrap Orchestrator 或 Connection State Machine,把初始化依赖说清楚。关闭流程也一样。如果业务要求先停止接收新请求,再停止 Consumer,等待 In-flight Job 完成,最后关闭数据库、Redis 和 Queue Connection,这种关系不应该依赖 Provider 恰好按照某个声明顺序销毁。
Major Upgrade 只是把原本隐藏的耦合暴露出来。
还有一些变化值得做定向回归
NestJS 12 还有一些没有 ESM、Standard Schema 那么醒目,但可能直接影响特定项目的变化。PipeTransform#transform 类型收紧了,ArgumentMetadata 和 ValidationPipe Error Output 的能力也有扩展。gRPC 异常、Kafka Pattern、WebSocket Gateway 对 Request Scope 的支持、WebSocket Disconnect 原因信息、Microservice Handler 的执行扩展点,以及 Express Adapter Graceful Shutdown 和 HTTP Error Mapping,都有面向特定场景的增强。CLI、SWC、Rspack 在 Build 和 Monorepo 上也继续补能力。
这些能力没有必要逐条当成独立改造项。判断是否重要只需要看项目有没有依赖对应能力,有就做 Targeted Regression,没有就不必因为 Release Note 出现了这个 Feature 而专门改项目。
从 NestJS 11 升级到 12,我会这样做
对于实际生产项目,我不建议从修改 package.json 开始。更稳妥的是先建立升级前基线。
先保留可以比较的旧版本事实
至少保存当前 Lockfile、Node.js Version、Build Result,以及 Unit Test、E2E Test。契约侧留下核心 REST Response Snapshot、OpenAPI Document、GraphQL Schema、Message Payload Sample 和 JSON Log Sample。运行侧记下关键 Metrics Baseline,还有当前启动和 Graceful Shutdown 行为。
这些东西决定升级以后我们有没有能力判断变化是正常 Migration,还是 Regression。
先统一 Node.js
不要只升级开发机。同时确认 Local、CI、Docker、Preview 和 Production 使用兼容的 Node.js 主版本。本地先核对 node --version 和 pnpm --version,镜像里也不要各写各的,例如统一到 FROM node:24。Node Runtime 不一致,是 ESM Migration 中非常常见的环境型问题。
先跑 dry-run
bash
pnpm add -D \
@nestjs/cli@latest \
@nestjs/schematics@latest
pnpm exec nest upgrade --dry-run
重点看 Migration Report 里有没有 GraphQL、NATS、Webpack、Joi、Testing、Config、Lifecycle、Custom Pipe 和 Logging 相关提示。这些条目通常对应后面最需要人工确认的行为变化。
再执行自动升级
bash
pnpm exec nest upgrade
随后第一件事不是 Commit,而是 git diff。重点看 package.json、Lockfile、nest-cli.json、tsconfig.json、tsconfig.build.json,以及 GraphQL Configuration、NATS Import、Config Validation 和 Testing Configuration。
自动 Migration 最有价值的地方,恰恰不是让我们不用 Review,而是把大量机械变化集中到一份容易 Review 的 Diff 中。
完整执行工程检查
至少执行 pnpm install、pnpm lint、pnpm build、pnpm test 和 pnpm test:e2e。Monorepo 要构建全部 Application 和 Library,不能只验证主入口能 Build。
根据技术栈做专项回归
REST 项目重点看 Route Conflict、Validation、Serialization、Error Response、Logging 和 Graceful Shutdown。有 GraphQL 时,把 Query、Mutation、GraphiQL、Subscription、Authentication 和 Reconnect 跑一遍。有 NATS 时,验证 Request-response、Event、Header、Serializer、Deserializer 和 Graceful Shutdown。如果依赖 Lifecycle,就检查 Database Connection,Redis、Queue Connection,Consumer Startup,Readiness 和 Shutdown Ordering。
这种专项回归比简单多跑几遍 Unit Test 更有意义,因为很多 Major Upgrade 问题恰恰发生在 Integration Boundary。
最后再灰度
NestJS 12 刚进入正式发布线,大型生产项目没有必要从 NestJS 11 直接全量切换。更合理的是开发、CI、测试、预发布都稳定以后,再进入少量 Instance Canary。
灰度阶段至少观察 5xx Error Rate,P95、P99 Latency,Startup Time,CPU、Memory,以及 Database、Redis、Kafka、NATS、WebSocket 连接,再加上 Queue Backlog、Retry、Graceful Shutdown Duration、Log Field 和 Alert 是否正常。
Major Upgrade 最危险的从来不是启动失败,启动失败反而最好发现。真正危险的是应用能启动、Test 大部分通过,但生产中的日志、生命周期、Subscription、Message Contract 或 Router Behavior 已经悄悄变化。
哪些项目可以优先升级
新项目没有太多历史负担,可以直接从 NestJS 12 开始。如果希望采用新的工程方向,可以按当前默认栈来选,也就是 NestJS 12、当前 Active LTS Node.js、ESM、Fastify、Vitest、oxlint、Rspack 或 SWC、Zod 和 Standard Schema。这不是唯一正确技术栈,只是更接近当前 NestJS 新项目默认方向。
普通 REST 项目如果主要使用 Express 或 Fastify、PostgreSQL、Redis、BullMQ、常规 Controller、Service 和 class-validator,整体升级风险通常比较容易控制。这种项目完全可以先升 NestJS,不动 ESM,不动 DTO,不动 Test Runner。把每次 Migration 的变量数量控制住,反而更容易找到问题。
哪些项目需要更谨慎
下面这些项目应该把升级当成一个正式 Migration Task,而不是一次 Dependency Bump。
- 使用 GraphQL Subscription
- 深度使用 NATS
- 有大量自定义 Serializer、Deserializer
- 依赖特定 Lifecycle Hook 顺序
- 存在复杂 Webpack Configuration
- 自定义 Nest CLI Builder
- 深度导入 CLI 或 Nest Package 内部文件
CommonJS、ESM、Jest依赖关系非常复杂- 生产告警依赖固定 JSON Log Field
- 大量自定义 Pipe、Metadata
Node.jsRuntime 暂时无法升级
这些项目不是不能升级,而是不能把 nest upgrade 当成 Migration 已经完成的证明。
总结
NestJS 12 最值得关注的地方,不是某一个新 Decorator,也不是单纯把 Package 从 CommonJS 改成 ESM。它真正体现的是 NestJS 工程边界正在发生几处明显变化。
Framework Package 已经全面进入 ESM 方向,但没有强迫现有 Application 一起迁移。这让我们可以把 Framework Upgrade 和 Module System Migration 拆开,降低大版本升级风险。
Standard Schema 正式进入请求验证、响应序列化和配置校验。NestJS 不再只有 Class DTO 这一条主路线,Zod、Valibot、ArkType 等 Schema-first 工具可以更自然地参与 Runtime Contract。
CLI 不再只是生成 Controller、Service 的脚手架。nest upgrade 开始承担 Major Migration 中大量机械工作,Rspack、Vitest、oxlint 和 ESM 也越来越明显地成为新项目方向。
框架开始把更多生产问题放到正式 API 中。Route Conflict、Machine-readable Error Code、Structured Logging、Graceful Shutdown 和官方 Observe,都在解决同一个问题,应用能启动并不代表生产行为一定正确。
所以对于现有 NestJS 11 项目,我不会建议一次升级同时完成 ESM、Vitest、Rspack、Zod 和 Observe 的全面改造。更稳妥的方式是先升级 Framework,把 Breaking Change 和生产行为验证清楚,再根据项目真实需求逐步采用新的默认能力。
NestJS 12 给我们的选择变多了,但真正重要的仍然是控制 Migration Scope。框架升级应该让系统获得新的能力,而不是把一次可控的大版本升级变成整个工程栈的重写。