NestJS 12: ESM packages, Standard Schema, and native observability

João RangelAugust 27, 202613 min read

In this article

The NestJS 12 was officially released on August 27, 2026 and is one of the deepest updates in the framework's history — but curiously, one that requires the least application code rewriting.

The version revolves around four fronts: ESM packages, first-class support for Standard Schema for validation and serialization, a CLI rebuilt from scratch, and native observability through the new package @nestjs/observe.

And there's an important point right from the start: CommonJS applications continue to function. Migrating your own code to ESM is entirely optional.

Among the highlights are:

  • All core packages are now ESM, with CommonJS continuing to function via require(esm);
  • Standard Schema in route decorators, allowing Zod, Valibot, and ArkType in @Body(), @Query(), and @Param();
  • Schema-based serialization with the new StandardSchemaSerializerInterceptor;
  • Native observability with the SDK @nestjs/observe;
  • Rebuilt CLI, with the new commands nest upgrade and nest deploy;
  • Rspack in place of webpack, oxlint in place of ESLint, and Vitest as the default in ESM projects;
  • Route conflict diagnosis, to identify shadowed or duplicated routes;
  • Machine-readable error codes in HTTP exceptions;
  • Structured logs by default in ConsoleLogger;
  • Node.js v20.19+ or v22.12+ as a minimum requirement.

Let’s understand what changes in practice.

The NestJS packages are now ESM

The most important architectural change in NestJS 12 is the migration of the core packages from CommonJS to ESM.

This discussion has been dragging on in the Node.js ecosystem for years, and the reason it finally unlocked is technical: support for require(esm) in modern versions of Node.js.

In practice, this means that a CommonJS project can continue loading packages that are ESM, without needing to rewrite imports, convert files, or change the build strategy.

Therefore, the official recommendation is straightforward: in most projects, nothing needs to be done.

Still, it’s worth reviewing three points that usually assume CommonJS implicitly:

  • Custom application bootstrap scripts;
  • Build tools and configurations;
  • Test runners and mocks that depend on the module format.

It’s also a good moment to eliminate deep imports — those imports that reach internal paths of Nest packages instead of using the public API. They have always been fragile and remain the kind of thing that breaks when changing module formats.

Another visible change is in project creation. The command:

nest new

now asks if you want to generate a CommonJS or ESM project. The choice also defines the rest of the toolchain, as we will see ahead.

Validation with Standard Schema directly in decorators

If you follow the TypeScript ecosystem, you’ve probably come across Standard Schema: a common specification that validation libraries implement to be used interchangeably.

NestJS 12 adopts this specification natively.

Route parameter decorators — @Body(), @Query(), @Param(), and @RawBody() — now accept a new schema option, designed for compatible libraries like Zod, Valibot, and ArkType:

@Post()
create(@Body({ schema: createUserSchema }) body: CreateUserDto) {
  return this.usersService.create(body);
}

@Get(':id')
findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {
  return this.usersService.findOne(id);
}

There’s an implementation detail worth understanding well: the decorator only attaches the metadata. It does not validate anything by itself.

What validates is the new pipe, which needs to be registered:

app.useGlobalPipes(new StandardSchemaValidationPipe());

This keeps the framework’s architecture consistent with what already existed: the decorator describes the intent, the pipe executes.

An important gain is that the same schemas feed the generation of OpenAPI. That is, you do not need to maintain one definition for validation and another for documentation.

And for those who already have a large base written in the traditional model, the message is reassuring: the flow with class-validator continues fully supported, with no plan for removal.

Serialization also gained a Standard Schema version

The same idea was carried to the request output path.

The new StandardSchemaSerializerInterceptor validates and transforms the response using the same schema ecosystem:

@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({ schema: userResponseSchema })
@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(id);
}

This solves a very concrete problem for production APIs: ensuring that the object returned to the client contains exactly the expected fields — and not whatever accidentally came from the database.

The choice between the two approaches is by use case:

  • ValidationPipe and ClassSerializerInterceptor for class-based DTOs;
  • Standard Schema versions when your schemas already exist in the project.

Native observability with @nestjs/observe

Perhaps the most strategic novelty of NestJS 12 is @nestjs/observe, the official observability SDK of the framework.

The difference compared to a generic APM is in the approach. Instead of instrumenting the HTTP server externally, the SDK connects to the very request lifecycle of Nest, through a new application option called instrument:

export const { ObserveModule, ObserveInstrument } = createObserveModule();

const app = await NestFactory.create(AppModule, {
  instrument: ObserveInstrument,
});

The practical consequence is that the data arrives described in terms of your application: controllers, providers, resolvers, and queue consumers — and not in terms of generic HTTP routes.

The automatic instrumentation covers:

  • HTTP;
  • GraphQL;
  • gRPC;
  • Microservices transports;
  • Queue consumers;
  • Cron executions.

No manual instrumentation of spans and no need to run a collector.

This is an opt-in and new feature, meaning: there’s nothing to migrate. Both nest new and nest upgrade can carry out the initial configuration with the flag --observe.

The @nestjs/config now validates via Standard Schema

The configuration module is no longer coupled to Joi.

The option validationSchema now accepts any schema compatible with Standard Schema:

ConfigModule.forRoot({
  validationSchema: z.object({
    NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
    PORT: z.coerce.number().default(3000),
  }),
});

Those who already use Joi do not need to rewrite anything, but must observe two points:

  • Update to Joi v18+, which is the first version to implement Standard Schema;
  • Move the library-specific configurations into validationOptions.libraryOptions.

Route conflict diagnosis

This is the kind of feature that only makes sense for those who have wasted time with the problem.

The routes of Nest are registered in the order of declaration. In order-sensitive adapters, this means that a route like:

@Get(':id')

can silently shadow a route declared later:

@Get('me')

The result is a request to /users/me arriving at the handler of /users/:id with the id worth the string "me". No error, no warning — just incorrect behavior.

NestJS 12 brings two opt-in options to expose this scenario:

const app = await NestFactory.create(AppModule, {
  routeConflictPolicy: { duplicate: 'error', shadow: 'warn' },
  routeResolutionStrategy: 'specificity',
});

Both maintain the previous behavior by default, so nothing changes unless you configure them explicitly.

Machine-readable error codes

Another small improvement with a large impact on daily life.

The HttpExceptionOptions now accepts an errorCode, which is serialized in the response body:

throw new BadRequestException('Password is too weak', { errorCode: 'WEAK_PASSWORD' });

With this, the client can branch logic on top of a stable identifier, instead of doing string comparison on the error message — which changes when someone corrects a comma or translates the text.

Structured logs in ConsoleLogger

The ConsoleLogger now treats objects passed after the message as structured parameters of the same log, and not as separate logs:

logger.log('User created', { userId: 1, email: '[email protected]' });

In JSON mode, this data is nested under params, or can be spread at the root with flattenParams.

The behavior is enabled by default. To revert to the previous format, simply configure structuredParams: false.

The NestJS CLI has been rebuilt

The @nestjs/cli v12 not only received new commands: it was rewritten.

The entire source code migrated to ESM, tests moved from Jest to Vitest, end-to-end tests were added for all commands, and command classes now receive typed context objects instead of untyped inputs and option arrays.

New commands

  • nest upgrade (alias update) — upgrades a v11 project to v12 and applies the mechanical steps of migration;
  • nest deploy — publishes the application in the cloud via Mau, installing @nestjs/mau on first use.

New toolchain standards

  • Rspack becomes the default bundler for monorepos. The flags --webpack and --webpackPath (and their equivalents in nest-cli.json) are deprecated in favor of --builder rspack;
  • oxlint replaces ESLint in generated projects;
  • Vitest is the default test runner in ESM projects; CommonJS projects continue with Jest;
  • bun becomes a supported package manager, alongside npm, yarn, and pnpm;
  • The schematic decorator now generates decorators in the preferred format Reflector.createDecorator(), and the schematic angular has been removed.

New options

  • nest build and nest start: --rspackPath [path], --emit-declarations (SWC), --no-type-check, and --silent;
  • nest build: --parallel [concurrency], to compile monorepo projects in parallel along with --all;
  • nest-cli.json: includeLibraryAssets, to copy library assets to the application build.

New features scattered across the rest of the framework

In addition to the main highlights, NestJS 12 brings a series of smaller improvements that solve very specific annoyances:

  • ValidationPipe error format — a new option controls the format of validation error responses;
  • gRPC exception filter — the GrpcExceptionFilter and exceptions specific to status map errors to the correct gRPC statuses, instead of returning UNKNOWN;
  • Kafka patterns by regular expression — @MessagePattern() and @EventPattern() accept a RegExp in Kafka transport;
  • WebSocket gateways with request scope — gateways now support request-scoped providers, with the socket injectable by the token REQUEST;
  • Disconnect reason — handleDisconnect can receive the reason for disconnection;
  • Pre-request hook in microservices — a new hook runs before the message handler is invoked;
  • Graceful shutdown in Express — the adapter drains ongoing requests when shutting down;
  • Error mapping for HTTP adapters — reworked in the core, in Express and in Fastify.

The graceful shutdown of Express, in particular, is one of those fixes that those running Kubernetes will appreciate: during a rollout, ongoing requests are no longer cut off mid-process.

What breaks when upgrading to NestJS 12

The team chose to concentrate incompatible changes outside of application code. Still, it’s worth knowing the list:

  • Node.js v20.19+ or v22.12+ — the 21.x line is not supported, and the upgrade command refuses to run on earlier versions;
  • Packages in ESM — normally nothing changes, thanks to require(esm); review custom bootstrap, bundler, and test runner;
  • Lifecycle hooks — are now invoked by the hierarchy level of the components; review assumptions about the order between related providers and modules;
  • NATS v3 — the nats package has been replaced by @nats-io/transport-node; messages are now serialized as JSON and custom deserializers receive the complete message read with msg.json();
  • GraphQL subscriptions — support for subscriptions-transport-ws was removed in favor of graphql-ws; the protocols are incompatible at the wire level, so clients need to be updated;
  • GraphiQL becomes the default GraphQL IDE — replace playground with graphiql;
  • @nestjs/config — validation via Standard Schema, with Joi v18+ and validationOptions.libraryOptions;
  • Pipe signatures — refined, with ArgumentMetadata now being generic; adjust custom pipes if the compiler complains;
  • ConsoleLogger with structured params enabled by default;
  • Webpack flows in the CLI deprecated, in favor of --builder rspack;
  • Schematic angular removed.

The good news is that most of this is treated automatically by nest upgrade.

How to upgrade to NestJS 12

The first step is to update the CLI, as it is the one that carries out the upgrade command:

npm i -g @nestjs/cli@latest

Then, from the root of the project:

nest upgrade

The command moves all @nestjs/* packages to the major compatible with v12 all at once and applies the mechanical parts of the migration:

  • Webpack options in nest-cli.json;
  • Renaming playground to graphiql in GraphQL and switching subscription transport;
  • Replacing the NATS package;
  • Validation options for @nestjs/config;
  • Updates to Jest and Joi.

At the end, it prints a report with everything that changed and everything that still needs manual review.

Before applying to a real project, it’s worth running:

nest upgrade --dry-run

to see that same report without touching any files.

And an important point: the command deliberately does not migrate your project to ESM, Vitest, or oxlint. These are the standards for new projects — existing projects adopt at their own pace.

As with any major update, the safe roadmap remains the same:

  • Run the entire suite of automated tests;
  • Validate authentication, guards, interceptors, and custom pipes;
  • Check microservices, queues, and scheduled jobs;
  • Test the build process and deployment in a staging environment before production.

Is it worth upgrading to NestJS 12?

For projects that are already on NestJS 11, the upgrade is much less risky than the major number suggests — precisely because the most radical changes occur in layers that the application normally does not touch.

What you gain just by upgrading:

  • Graceful shutdown in Express;
  • Correct error mapping in HTTP adapters;
  • Appropriate statuses in gRPC exceptions;
  • Structured logs in ConsoleLogger.

What is optional adoption, and can come later at a slow pace:

  • Migrate the project to ESM;
  • Switch from class-validator to Zod, Valibot, or ArkType;
  • Adopt Vitest, oxlint, and Rspack;
  • Enable @nestjs/observe.

The main practical block is not the framework, but the runtime: if your environment still runs a version of Node.js prior to v20.19, that is the first thing to resolve.

What NestJS 12 shows about the direction of the framework

Looking at the whole, NestJS 12 has a fairly clear line.

On one hand, the framework is aligning with the ecosystem instead of maintaining its own solutions: ESM instead of CommonJS, Standard Schema instead of coupling to a specific validation library, graphql-ws instead of a legacy transport.

On the other hand, it is modernizing the toolchain with high-performance native implementations — Rspack and oxlint follow exactly the same trend that Turbopack represents in the Next.js world.

And there is a third front, more silent: several innovations in this version exist to make the system behavior observable. Route conflicts that cease to be invisible. Error codes that the client can interpret. Logs that become structured data. Observability that speaks the language of your controllers.

These are less flashy improvements than a new API, but they tackle precisely the type of problem that tends to be expensive in production.

Frequently asked questions about NestJS 12

When was NestJS 12 released?

NestJS 12.0.0 was officially released on August 27, 2026.

What’s new in NestJS 12?

The main new features are ESM packages, first-class support for Standard Schema for validation and serialization, the rebuilt CLI with the commands nest upgrade and nest deploy, and native observability through the SDK @nestjs/observe.

Do I need to migrate my application to ESM in NestJS 12?

No. The Nest packages have become ESM, but CommonJS applications continue to function thanks to require(esm) in modern versions of Node.js. Migrating your own code is optional.

What is the minimum version of Node.js for NestJS 12?

It is necessary to have Node.js v20.19+ or v22.12+. The 21.x line is not supported, and the nest upgrade command refuses to run on earlier versions. The recommendation is to use the most recent active LTS.

Will class-validator be removed from NestJS?

No. Support for Standard Schema is an alternative, not a replacement. The decorator-based flow with class-validator continues to be fully supported, with no plan for removal.

How to upgrade a NestJS 11 project to 12?

Update the CLI with npm i -g @nestjs/cli@latest and run nest upgrade from the root of the project. Run first with --dry-run to see the report of what would change without touching the files.

Does NestJS 12 force the use of Vitest and oxlint?

No. Vitest, oxlint, and ESM are only the standards for new projects generated by the CLI. The nest upgrade command does not convert existing projects to them.

What is @nestjs/observe?

It is the official observability SDK for NestJS. It connects to the request lifecycle of the framework itself through the instrument option and automatically instruments HTTP, GraphQL, gRPC, microservices transports, queue consumers, and cron executions — without manual instrumentation of spans and without a collector.

Keep learning with Hcode

Sign up for the course platform and follow the next blog guides.