What's New in NestJS 12?
NestJS 12 landed at the end of August. It is one of the biggest releases in years: the framework moves to ESM, validation gets Standard Schema support, the CLI is rebuilt, and there is an official observability SDK.
The good news: your existing CommonJS projects keep working without a rewrite.
Let's dive in.
Requirements
- Node.js v20.19+ or v22.12+
- Node.js v21.x is not supported
1. ESM-first Packages
All core Nest packages now ship as ECMAScript modules.
CommonJS apps keep working thanks to Node.js require(esm) support. Migrating your own code to ESM is optional.
In an ESM project, main.ts looks like this:
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
const app = await NestFactory.create(AppModule);
await app.listen(3000);
Note the
.jsextension on relative imports and the top-levelawait.
2. Validation with Standard Schema
One of the most exciting additions. @Body(), @Query(), @Param() and @RawBody() now accept a schema option. Zod, Valibot, ArkType — any Standard Schema-compatible library works.
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
@Post()
create(@Body({ schema: createUserSchema }) body: CreateUserDto) {
return this.usersService.create(body);
}
Register StandardSchemaValidationPipe to run the validation.
class-validatorstill works. It's just no longer the only option.
3. Response Serialization
The same idea applies to outgoing responses via StandardSchemaSerializerInterceptor:
@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({ schema: userResponseSchema })
@Get(":id")
findOne(@Param("id") id: string) {
return this.usersService.findOne(id);
}
Sensitive fields like passwords are stripped at the schema level.
4. ConfigModule Moves to Standard Schema
@nestjs/config is no longer tied to Joi:
ConfigModule.forRoot({
validationSchema: z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().default(3000),
}),
});
Using Joi? Upgrade to Joi v18+ and move your settings under validationOptions.libraryOptions.
5. Machine-Readable Error Codes
Ever matched on error message text in the frontend? No more:
throw new BadRequestException("Password is too weak", {
errorCode: "WEAK_PASSWORD",
});
The message can change or be translated; the errorCode stays stable.
6. Structured Logging
ConsoleLogger now treats plain objects as structured parameters:
this.logger.log("User signed in", { userId: 1, method: "oauth" });
In JSON mode, parameters are nested under a params key by default. Enable flattenParams to spread them at the root.
To restore the old behavior:
structuredParams: false.
7. Route Conflict Diagnostics
A route silently shadowing another one is a hard bug to find. v12 lets you detect it explicitly:
const app = await NestFactory.create(AppModule, {
routeConflictPolicy: { duplicate: "error", shadow: "warn" },
routeResolutionStrategy: "specificity",
});
8. Native Observability: @nestjs/observe
The official SDK hooks directly into the request lifecycle, without patching the HTTP server:
export const { ObserveModule, ObserveInstrument } = createObserveModule();
const app = await NestFactory.create(AppModule, {
instrument: ObserveInstrument,
});
Automatically covers:
- HTTP, GraphQL, gRPC
- Microservice transports
- Queue consumers and cron jobs
9. New CLI and Toolchain
| What | v11 | v12 (ESM project) |
|---|---|---|
| Testing | Jest | Vitest |
| Linter | ESLint | oxlint |
| Monorepo bundler | Webpack | Rspack |
| Package managers | npm/yarn/pnpm | + bun |
CommonJS projects stay on Jest and ESLint. Webpack is now deprecated — use --builder rspack.
New commands:
nest upgrade— migrates a v11 project to v12 automaticallynest deploy— deploys your app
New build options: --rspackPath, --emit-declarations, --no-type-check, --silent, --parallel.
10. Microservices, GraphQL and WebSockets
- NATS v3 — via the
@nats-io/transport-nodepackage - Kafka —
@MessagePattern()and@EventPattern()accept RegExp - Pre-request hook — runs before a message handler is invoked
- gRPC — exception filters with proper status code mapping
- GraphQL — GraphiQL is now the default IDE
- WebSockets — request-scoped gateways and a disconnect reason in
handleDisconnect - Express — graceful shutdown drains in-flight requests
Breaking Changes
Check these before upgrading:
- Node.js version — v20.19+ or v22.12+
- Lifecycle hooks — now invoked by module hierarchy level. Review init/teardown ordering
- GraphQL —
subscriptions-transport-wsremoved, migrate tographql-ws.playground→graphiql - NATS — packets are serialized as JSON strings
- Custom pipes —
ArgumentMetadatais now generic, update signatures
How to Upgrade
npm i -g @nestjs/cli@latest @nestjs/schematics@latest
npm i @nestjs/cli@latest @nestjs/schematics@latest
nest upgrade --dry-run
nest upgrade
Start with
--dry-runto review changes. The CLI handles mechanical changes; custom code still needs your review.
Moving to ESM (optional)
- Add
"type": "module"topackage.json - In
tsconfig.json, set"module"and"moduleResolution"to"nodenext" - Add
.jsextensions to relative imports - Replace
__dirnamewithimport.meta.dirname
Conclusion
- NestJS 12 pushes the ecosystem toward ESM, Standard Schema and faster tooling.
- The upgrade path is smooth:
nest upgradedoes most of the work. - My recommendation: start new projects with ESM + Zod, and upgrade existing ones to v12 while staying on CommonJS first.
Comments