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 .js extension on relative imports and the top-level await.

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-validator still 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

Whatv11v12 (ESM project)
TestingJestVitest
LinterESLintoxlint
Monorepo bundlerWebpackRspack
Package managersnpm/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 automatically
  • nest 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-node package
  • 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:

  1. Node.js version — v20.19+ or v22.12+
  2. Lifecycle hooks — now invoked by module hierarchy level. Review init/teardown ordering
  3. GraphQLsubscriptions-transport-ws removed, migrate to graphql-ws. playgroundgraphiql
  4. NATS — packets are serialized as JSON strings
  5. Custom pipesArgumentMetadata is 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-run to review changes. The CLI handles mechanical changes; custom code still needs your review.

Moving to ESM (optional)

  1. Add "type": "module" to package.json
  2. In tsconfig.json, set "module" and "moduleResolution" to "nodenext"
  3. Add .js extensions to relative imports
  4. Replace __dirname with import.meta.dirname

Conclusion

  • NestJS 12 pushes the ecosystem toward ESM, Standard Schema and faster tooling.
  • The upgrade path is smooth: nest upgrade does most of the work.
  • My recommendation: start new projects with ESM + Zod, and upgrade existing ones to v12 while staying on CommonJS first.

Resources

Comments