DEV Community

Cover image for NestJS 12 Deep Dive — Full ESM Migration, Standard Schema Route Validation, and the Vitest·oxlint·Rspack Toolchain
daniel jeong
daniel jeong

Posted on • Originally published at manoit.co.kr

NestJS 12 Deep Dive — Full ESM Migration, Standard Schema Route Validation, and the Vitest·oxlint·Rspack Toolchain

If you've written Node.js backends, you've almost certainly been stopped cold by ERR_REQUIRE_ESM. That error — thrown the moment you try to require() an ESM-only package from a CommonJS project — has been one of the most time-wasting sources of friction in the Node.js ecosystem for years. On April 30, 2026, NestJS published a draft PR (#16391) outlining the scope of v12.0.0, with a decision that ends that friction head-on: a full ESM migration of every package. Alongside it, route decorators gain Standard Schema validation, and the default toolchain is swapped from Jest, ESLint, and Webpack to Vitest, oxlint, and Rspack. This post breaks down what NestJS 12 changes — and how existing projects should prepare — using the official PR and InfoQ reporting as primary sources.

NestJS 12 changes overview — migration from CJS to ESM-native, toolchain modernization, Standard Schema route validation

1. NestJS 12 at a Glance

NestJS is a progressive Node.js framework for TypeScript server-side applications, providing a modular architecture on top of Express or Fastify. With over 75,000 GitHub stars, it's an enterprise standard. v12 targets early Q3 2026, and its core distills into four changes.

Area v11 (current) v12 (early Q3 2026)
Module system CommonJS ESM (all packages migrated)
Input validation class-validator centric Standard Schema option (Zod, Valibot, ArkType)
Testing Jest Vitest + OXC (default for ESM projects)
Linter ESLint oxlint (default everywhere)
Bundler Webpack Rspack (Webpack deprecated)

Framework creator Kamil Myśliwiec stated in the PR that the transition "should not introduce major breaking changes for existing projects." There are a few minor breaking changes across other packages, but "nothing significant." The character of this major release is therefore not an API-breaking upgrade, but an infrastructure upgrade that modernizes the module system and developer tooling. The decorator-based programming model for controllers, providers, and modules stays the same, so most application code runs untouched. The center of gravity is the build, run, and validation pipeline — not the application surface.

2. Why ESM Now — require(esm) Stability as the Premise

ESM migration was deferred for a clear reason: with most of the ecosystem still CommonJS-based, going ESM-only would force users to absorb mandatory import syntax, top-level await constraints, and the absence of __dirname all at once. Myśliwiec pinpointed exactly this:

"The availability of require(esm) was the missing piece that made the move to ESM practical — without it, the migration wouldn't have made much sense." — Kamil Myśliwiec

require(esm) is a Node.js feature that lets you load synchronous ES modules from CommonJS via require(). Introduced experimentally in 2024 and thoroughly battle-tested, it's now unflagged across all supported LTS lines (v20.19.0+, v22.12.0+) and marked stable. The key was dispelling the misconception that ESM is inherently asynchronous, enabling syntax-based synchronous evaluation of ESM. As a result, existing CommonJS projects can require() NestJS 12's ESM packages directly, minimizing migration friction.

// NestJS 12 — CommonJS projects can require ESM packages directly
// (Node.js v20.19+ / v22.12+ : require(esm) stable)
const { NestFactory } = require('@nestjs/core'); // ESM package, still works

// New ESM projects naturally use import
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js'; // ESM: explicit (.js) extension recommended
Enter fullscreen mode Exit fullscreen mode

⚠️ Caution: require(esm) only applies to synchronously evaluable ESM. If the target ESM uses top-level await, require() throws ERR_REQUIRE_ASYNC_MODULE. In new ESM projects, relative imports must specify the extension (.js), and you must use import.meta.url instead of __dirname.

3. Standard Schema — Beyond class-validator

The biggest change backend developers will feel in NestJS 12 is Standard Schema support in route decorators. Every route decorator — @Body, @Query, @Param — accepts a new schema option that takes a Standard Schema–compatible object. The same capability extends to the serializer interceptor.

3.1 What Is Standard Schema?

Standard Schema is not a validation library but a roughly 60-line TypeScript interface. Designed jointly by the creators of Zod, Valibot, and ArkType, it works by having each library expose a ~standard property on its schemas, so any tool that understands Standard Schema can validate data without knowing which library produced the schema. Its core method is a single validate(unknown) that returns either a typed value on success or an array of issues on failure.

The value of this interface is eliminating vendor lock-in. It reduces the relationship between validation libraries and consuming tools from N×M to N+M. Start with familiar Zod, move to smaller-bundle Valibot, or adopt type-level ArkType — all without rewriting your route or handler code. NestJS validation has long been tightly bound to class-validator and class-transformer, a combination that depends on reflect-metadata for decorator metadata and requires DTOs to be declared as classes. Standard Schema support turns that constraint into a choice — especially valuable for teams that want a single schema source driving both runtime validation and static types, or for monorepos sharing the same Zod schema across frontend and backend.

3.2 In Practice — Route Validation with a Zod Schema

// NestJS 12 — use a Standard Schema directly instead of a class-validator DTO
import { Controller, Post, Body } from '@nestjs/common';
import { z } from 'zod';

// Define a Zod schema (Zod already implements Standard Schema's ~standard)
const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  role: z.enum(['admin', 'instructor', 'student']).default('student'),
});

// Infer the type from the schema — no separate DTO class needed
type CreateUserDto = z.infer<typeof CreateUserSchema>;

@Controller('users')
export class UsersController {
  @Post()
  create(@Body({ schema: CreateUserSchema }) dto: CreateUserDto) {
    // dto is already validated, parsed, and type-safe
    return { created: dto.email, role: dto.role };
  }
}
Enter fullscreen mode Exit fullscreen mode
Approach Validation definition Characteristics
class-validator (existing) Decorators + DTO class Runtime metadata, depends on reflect-metadata
Zod z.object({...}) Most widely adopted, rich ecosystem
Valibot Functional pipeline Excellent tree-shaking, small bundles
ArkType Type-string syntax Type-system-level validation, high performance

💡 Tip: class-validator and Standard Schema are not mutually exclusive. Existing DTO-based validation still works in NestJS 12, so a safe strategy is to adopt Standard Schema for new endpoints first. Both validation packages are also flagged as optional — if you don't use them, they're never installed.

4. Toolchain Modernization — Rust Arrives

NestJS 12 swaps out its default tools en masse for faster feedback loops, aligning with the broader trend of Rust-powered JavaScript tooling becoming the standard. Notably, all three replacements — Vitest's transform layer, oxlint, and Rspack — share the same direction: native speed and ESM friendliness. Jest, ESLint, and Webpack were optimized for the CommonJS era; in ESM-native projects their configuration complexity and cold-start costs grew, accumulating with codebase size. The v12 toolchain swap is less a trend-chase than a necessary counterpart to the ESM migration.

4.1 Jest → Vitest (+ OXC)

All repositories and sample projects have migrated from Jest to Vitest, with OXC providing TypeScript decorator support. New ESM projects use Vitest by default, while CJS schematics continue to use Jest. Vitest's strengths are a fast Vite-based watch mode and native ESM execution.

# NestJS 12 CLI — prompts for module system at project creation
$ nest new my-api
? Which module system do you want to use?
  > ESM  (Vitest + oxlint default)
    CommonJS  (Jest + existing tools)

# With ESM selected, the test runner is Vitest
$ npm run test     # runs vitest
Enter fullscreen mode Exit fullscreen mode

4.2 ESLint → oxlint, Webpack → Rspack

For linting, oxlint replaces ESLint across all projects. Written in Rust as the linter of the OXC toolchain, oxlint delivers checks tens of times faster on large codebases. For bundling, Rspack replaces Webpack, which is now deprecated. Rspack aims to be a drop-in replacement with a Webpack-compatible API and significantly faster build times.

⚠️ Caution: In roadmap discussions, whether the default bundler will be Vite + SWC instead of Rspack is not yet settled ("nothing is set in stone"). Requests to add Bun and Biome as CLI options exist but aren't officially adopted. Packages are expected to ship under the next npm tag before the stable release, so validating with next builds before production adoption is recommended.

5. Other Changes

Beyond the headline changes, several improvements affect real-world work. In microservices, the migration to NATS v3 follows the latest message transport client API; the Express adapter's graceful shutdown support lets in-flight requests finish safely during zero-downtime deployments and rolling updates. The WebSocket disconnect reason parameter lets you distinguish termination causes at the code level, useful for refining reconnection logic and observability.

Area Change
Microservices NATS v3 migration
Express adapter Graceful shutdown support
WebSocket disconnect reason parameter
Pipes Improved transform type safety
Exceptions Custom errorCode option in HttpExceptionOptions
Website Full official site redesign planned

6. Preparing to Migrate — ManoIT's Recommended Strategy

A dedicated v11→v12 migration guide isn't published yet. But the PR's direction is clear, so ManoIT recommends a phased preparation for internal NestJS backends.

First, align your runtime to Node.js 22 LTS. You need v22.12+ or v20.19+ — where require(esm) is stable — to fully reap v12's ESM benefits. Second, adopt Standard Schema (Zod) validation for new endpoints to gradually reduce class-validator dependence. Third, adopt Vitest and oxlint locally ahead of time to shorten CI feedback loops and narrow the gap with v12's default toolchain. Fourth, run regression tests on next-tag builds in staging before the stable release to surface minor breaking changes early. Fifth, batch-align package upgrades with npm-check-updates, but pin core dependencies for predictability.

# Prepare for v12 — apply now on your current v11 project
node -v                          # confirm v22.12+ or v20.19+ (require(esm) stable)
npm i -D vitest @vitest/coverage-v8   # adopt alongside Jest, then migrate gradually
npm i -D oxlint                  # run alongside ESLint, compare speed, then switch
npm i zod                        # introduce Standard Schema validation (new endpoints)

# After GA: pre-validate with the next tag
npm i @nestjs/core@next @nestjs/common@next
Enter fullscreen mode Exit fullscreen mode

💡 Tip: The most common ESM pitfalls are missing extensions on relative imports and use of __dirname. Setting tsconfig's moduleResolution to NodeNext and appending .js extensions while still on v11 dramatically reduces the code changes needed at v12.

7. Closing — The ESM Era for Backend Frameworks

NestJS 12's message is clear: with require(esm) stable, the last obstacle blocking the Node.js ecosystem's ESM migration is gone, and an enterprise standard framework fired the starting gun. ESM-native migration, freedom of validation libraries via Standard Schema, and modernization toward a Rust-based toolchain look like independent improvements, but they point in one direction: a faster, less locked-in, standards-faithful backend. GA lands in early Q3 2026, but the core lessons — align to Node 22 LTS, adopt schema validation incrementally, pre-adopt Vitest and oxlint — can start today on your v11 project. With preparation, migration won't be an event; for ready teams, it'll be a one-line dependency update.


This post was written by the ManoIT tech blog automation pipeline, cross-verifying the official NestJS PR #16391, InfoQ reporting, and the Standard Schema specification. Version and timeline details are accurate as of the writing date (2026-06-05) and may change at GA — re-check the official docs before applying. · AI writing assistance: Claude (Anthropic)


Originally published at ManoIT Tech Blog.

Top comments (0)