NestJS 12.0.0 shipped on August 27, 2026, and the line has moved briskly since: 12.2.0 landed October 19, endoflife.date logs 12.4.0 on November 6, and newreleases already lists 12.4.2. The v12.0.0 release notes bundle four things that rarely travel together: every core package rebuilt as ESM, Standard Schema validation wired into the route decorators, a rebuilt CLI with new defaults, and a first-party observability SDK. For a v11 shop the headline is quieter than the changelog: most CommonJS apps keep running, and the upgrade is largely one command plus a careful read of its report.
The packages went ESM; your code can stay where it is
All core Nest packages now ship as ESM. What makes that survivable is Node's require(esm), which lets CommonJS load an ES module synchronously; it is also why the floor rose to Node.js 20.19+ or 22.12+; 21.x is unsupported, and nest upgrade refuses older runtimes. When the roadmap surfaced in April, Kamil Myśliwiec called require(esm) "the missing piece that made the move to ESM practical" (InfoQ).
The release notes promise that most CommonJS apps keep working, and "most" is doing honest work there. Satellite packages such as @nestjs/config are pure ESM with exports maps, so deep imports into build output stop resolving (@nestjs/config 12 notes). CommonJS-minded bootstrap scripts, bundlers and test runners deserve a look. The sharpest edge sits in the schematics release notes: Jest can only require() the ESM-only Nest 12 packages on Node.js 24.9 or later. A CommonJS project on Jest and Node 22 should settle that before upgrading.
Moving the app itself to ESM is optional. New projects default to it, generated with TypeScript 6, module and moduleResolution set to nodenext, resolvePackageJsonExports, isolatedModules, target: ES2023, and "type": "module" in package.json. Under nodenext, the type field decides what tsc emits. Older projects on module: commonjs need the whole diff below; projects already on nodenext need only the type field and explicit extensions on relative imports:
1// package.json 2+ "type": "module", 3 4// tsconfig.json (compilerOptions) 5- "module": "commonjs", 6+ "module": "nodenext", 7+ "moduleResolution": "nodenext", 8+ "resolvePackageJsonExports": true, 9+ "isolatedModules": true, 10+ "target": "ES2023", 11 12// src/app.module.ts: relative imports need the emitted extension 13- import { UsersModule } from './users/users.module'; 14+ import { UsersModule } from './users/users.module.js';
Code leaning on CommonJS globals such as __dirname or require.resolve needs a second pass.
Standard Schema reaches the route decorators
Standard Schema is the shared ~standard.validate interface that Zod, Valibot and ArkType implement. In v12, @Body(), @Query(), @Param() and @RawBody() accept a schema option, the same schemas feed OpenAPI generation, and class-validator stays fully supported with no removal planned (release notes). A class-validator DTO and its schema-first replacement:
1// src/users/users.controller.ts 2import { Body, Controller, Get, Param, Post } from '@nestjs/common'; 3import { z } from 'zod'; 4 5// Before: a DTO class plus ValidationPipe 6// export class CreateUserDto { 7// @IsEmail() email!: string; 8// @IsString() @Length(1, 100) name!: string; 9// } 10 11const createUserSchema = z.object({ 12 email: z.email(), 13 name: z.string().min(1).max(100), 14}); 15type CreateUserDto = z.infer<typeof createUserSchema>; 16 17@Controller('users') 18export class UsersController { 19 @Post() 20 create(@Body({ schema: createUserSchema }) body: CreateUserDto) { 21 return { created: body.email }; 22 } 23 24 @Get(':id') 25 findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) { 26 return { id }; 27 } 28}
The schema needs a pipe to act on it. Per its published typings, the options are transform (default true), validateCustomDecorators (default false), validateOptions, errorHttpStatusCode (default 400) and an exceptionFactory that receives the issues.
1// src/main.ts (ESM project, so top-level await is allowed) 2import { StandardSchemaValidationPipe, UnprocessableEntityException } from '@nestjs/common'; 3import { NestFactory } from '@nestjs/core'; 4import { AppModule } from './app.module.js'; 5 6const app = await NestFactory.create(AppModule); 7app.useGlobalPipes( 8 new StandardSchemaValidationPipe({ 9 exceptionFactory: (issues) => 10 new UnprocessableEntityException(issues.map((issue) => issue.message)), 11 }), 12); 13await app.listen(process.env.PORT ?? 3000);
Two footnotes. Those typings are from @nestjs/common 11.1.19, so the pipe predates v12; the major makes it the documented path and adds the outbound half, StandardSchemaSerializerInterceptor with @SerializeOptions({ schema }). And in the alphas one tester saw ArkType failures surface as 500s, traced to ArkType and taken upstream; test your error path.
@nestjs/config, which jumped from 4.x to 12.0.0, follows suit. validationSchema is now typed as StandardSchemaV1, Joi needs v18+, and library-specific options move under libraryOptions (config notes):
1// src/app.module.ts 2import { ConfigModule } from '@nestjs/config'; 3import Joi from 'joi'; 4import { z } from 'zod'; 5 6// Before: @nestjs/config 4.x 7ConfigModule.forRoot({ 8 validationSchema: Joi.object({ PORT: Joi.number().default(3000) }), 9 validationOptions: { allowUnknown: false, abortEarly: true }, 10}); 11 12// After: @nestjs/config 12.x with Joi 18+ 13ConfigModule.forRoot({ 14 validationSchema: Joi.object({ PORT: Joi.number().default(3000) }), 15 validationOptions: { libraryOptions: { allowUnknown: false, abortEarly: true } }, 16}); 17 18// After: or any other Standard Schema 19ConfigModule.forRoot({ 20 validationSchema: z.object({ PORT: z.coerce.number().default(3000), DATABASE_NAME: z.string() }), 21});
Error messages are now formatted as path: message lines, so tests matching the old Joi string will fail, and undeclared variables stripped by z.object() are merged back into the result.
A new toolchain, for new projects
The v12 CLI is itself a native ES module, pinned to TypeScript ~6.0.2. nest new asks which module system you want: ESM comes with Vitest (vitest.config.ts and vitest.config.e2e.ts), CJS with Jest, whose config moves to jest.config.ts; --type cjs keeps the classic layout (schematics notes). oxlint replaces ESLint with a plain "lint": "oxlint src/ test/". Rspack becomes the default monorepo builder, webpack drops to optional peer dependencies, and --webpack is deprecated in favour of --builder rspack. Generators append .js to relative imports in ESM projects, the angular schematic is gone, and a new nest deploy hands off to the Mau platform.
The floors disagree by tool: the CLI keeps engines.node >= 20.11, the schematics require Node 22.12.0, and nest upgrade writes >=20.19.0 into your project. Run tooling on 22.12+ and the contradiction disappears. The schematics also declare a typescript >= 6.0.0 peer dependency; users in the tracking thread report the CLI may not work with TypeScript 7, which lacks the programmatic API it needs.
Observability, with a meter attached
@nestjs/observe hooks into Nest's own lifecycle through a new instrument option rather than patching the HTTP server. The wiring, per the official SDK docs:
1// src/app.module.ts 2import { Module } from '@nestjs/common'; 3import { createObserveModule } from '@nestjs/observe'; 4 5export const { ObserveModule, ObserveInstrument } = createObserveModule(); 6 7@Module({ 8 imports: [ 9 ObserveModule.forRoot({ 10 appKey: process.env.OBSERVE_APP_KEY!, 11 appSecret: process.env.OBSERVE_APP_SECRET!, 12 serviceId: 'cats-app', 13 }), 14 ], 15}) 16export class AppModule {} 17 18// src/main.ts then passes the instrument at bootstrap: 19// import { AppModule, ObserveInstrument } from './app.module.js'; 20// const app = await NestFactory.create(AppModule, { instrument: ObserveInstrument });
With FastifyAdapter the options object is the third argument; pass it second and it is silently dropped. Coverage spans controllers, resolvers, queue consumers, gateways, driver-level queries for pg, mysql2, mssql and mongodb, and outbound HTTP. The SDK only needs @nestjs/core 11.1.4 or later, so it is not a v12 exclusive.
The fine print is commercial. The docs state that observe-api.nestjs.com is the only collector the hosted service runs; an otlp export option appears in the configuration table without further detail. Log forwarding needs the Pro plan and @Objective() SLOs need Scale or Enterprise, with no pricing on that page. Scout Monitoring, a rival vendor, warned of "vendor lock-in at the framework level" (InfoQ). It is opt-in, which is the part that matters.
The breaking changes that will bite
From the release notes: lifecycle hooks now run by component hierarchy level, so review init and teardown ordering, including in tests. NATS moves to v3 (@nats-io/transport-node), with payloads read via msg.json(). ConsoleLogger treats objects after the message as structured params by default; structuredParams: false restores the old output. Pipe signatures were refined and ArgumentMetadata is now generic, which touches custom pipes. GraphQL drops subscriptions-transport-ws and makes GraphiQL the default IDE:
1// src/app.module.ts (@nestjs/graphql, Apollo driver) 2 GraphQLModule.forRoot<ApolloDriverConfig>({ 3 driver: ApolloDriver, 4 autoSchemaFile: true, 5- playground: true, 6- subscriptions: { 'subscriptions-transport-ws': true }, 7+ graphiql: true, 8+ subscriptions: { 'graphql-ws': true }, 9 }),
The protocols are wire-incompatible, so clients move too. The HTTP adapters stay on the same majors; 12.2.0 pinned express 5.3.0 and fastify 5.12.7.
Then there is 12.2.0, a minor that changes defaults: Express and Fastify cap fieldNestingDepth at 32 and fieldArrayIndexLimit at 1000, ValidationPipe sets validationError.target to false, @Sse() caps pending data at 4M characters, and WebSocket adapters cap concurrent messages per client at 1000. Event handlers that reply to ClientProxy#send() now log a deprecation; the next major stops them.
Upgrade checklist
- Run production on Node 20.19+ or 22.12+, tooling and CI on 22.12+, and Node 24.9+ if a CommonJS project stays on Jest.
- Install the new CLI and read the
nest upgrade --dry-runreport first. - Run
nest upgrade; it will not move you to ESM, Vitest or oxlint. - Move to TypeScript 6 and Jest 30 if you stay on Jest.
- Fix the manual items: hook ordering, NATS, GraphQL clients, log parsers expecting the old
ConsoleLoggershape, custom pipe typings. - Bump Joi to 18+, move config options under
libraryOptions, update config-error assertions. - Read the 12.2.0 defaults before allowing
^12ranges. - Later, as separate pull requests: ESM, Vitest, and schema-first validation, module by module.
1node --version # must be 20.19+ or 22.12+; 21.x is refused 2npm i -g @nestjs/cli@latest 3nest upgrade --dry-run # report only, no file changes 4nest upgrade # add --observe to wire @nestjs/observe as well
The 30,000-foot read
The Nest team did the hard part where users cannot see it. The packages crossed to ESM, and require(esm) ferries everyone else without forcing the trip. What you control is narrow and concrete: your Node version, your test runner, and the order of the optional migrations. What you do not control is the ecosystem's drift toward ESM, Vitest and schema-first validation, now the default for anyone starting fresh. v11 lost active support the day v12 shipped and keeps only security fixes, so waiting is a choice with a slowly rising price.
Observe is the one piece that is also a business, and it is fine as long as it stays a choice. Watch whether the OTLP path earns real documentation, whether 12.x minors keep moving defaults, and when the toolchain makes peace with TypeScript 7.
Sources: endoflife.date: NestJS (12.0.0 release date, 12.4.0 date, v11 support status); NestJS v12.0.0 GitHub release notes, via newreleases (ESM, Standard Schema decorators and serializer, Observe, breaking changes, CLI changes); @nestjs/core 12.0.0 notes, via newreleases (Node.js floors, 12.4.2 listing); @nestjs/core 12.2.0 notes, via newreleases (12.2.0 date, new defaults, Express and Fastify versions, deprecation); @nestjs/cli 12.0.0 notes (native ESM CLI, TypeScript ~6.0.2, Rspack, nest upgrade flags, nest deploy, engines); @nestjs/schematics 12.0.0 notes (ESM/CJS scaffolding, generated tsconfig, Vitest, Jest 30 and Node 24.9 note, oxlint, TypeScript peer, Node 22.12 requirement); @nestjs/config 12.0.0 notes (Standard Schema validationSchema, libraryOptions before/after, error format, ESM exports); StandardSchemaValidationPipe typings, @nestjs/common 11.1.19 (pipe options and defaults); NestJS Observe SDK docs (wiring, Fastify caveat, coverage, collector, plans, version requirements); NestJS v12 tracking PR #16391 (alpha history, ArkType report, TypeScript 7 reports); InfoQ, April 30 (roadmap, Myśliwiec quote); InfoQ, October 19 (Observe reception, Scout Monitoring quote). Corrections to circulating leads: 12.2.0 did ship October 19 but is not the latest 12.x; PR #16391 is a release-tracking thread, not a single merged feature PR. CAVEAT: GitHub's release pages and the official migration guide could not be opened from this session, so release notes are cited through the newreleases mirror and the breaking-change list comes from the release notes rather than the guide; the 12.0.0 date comes from endoflife.date because the mirror shows only relative dates. No framework-wide TypeScript minimum is stated; TypeScript 6 is what generated and upgraded projects get. The GraphQL graphiql: true and 'graphql-ws' option shapes follow the release-note wording and were not checked against the @nestjs/graphql docs. The Jest and Node 24.9 constraint and the TypeScript 7 incompatibility are single-sourced (schematics notes; user reports). Running class-validator and Standard Schema pipes side by side during migration is not documented in anything I opened; test it. No chart: the release offers no official comparable figures.