NestJS published "NestJS v12 Roadmap: Full ESM Migration, Standard Schema Validation and Modernised Toolchain" on April 30, 2026. The primary change is the full migration from CommonJS to ESM, accompanied by support for Standard Schema to uniformly handle libraries like Zod and Valibot, as well as a toolchain overhaul transitioning from Webpack to Vite and Rolldown.
NestJS is widely adopted in Japan as an API server framework for enterprise systems and SaaS platforms, and we maintain it for multiple clients. Codebases built on CommonJS have accumulated over several years, making v12 a major update that definitively requires migration work. This article outlines how to handle the migration to NestJS v12 in custom development, from auditing tasks to planning a phased rollout.
Why a "full ESM migration" matters right now
Dual maintenance of ESM (ECMAScript Modules) and CommonJS has been a chronic source of friction across the Node.js ecosystem since the early 2020s. Entering 2026, major libraries have begun switching exclusively to ESM, and remaining on CommonJS increasingly leads to situations where "the latest versions cannot be used."
| Benefits of ESM migration | Impact on custom development projects |
|---|---|
| Improved tree-shaking efficacy | Bundle size reduced by 20–40%, shortened Lambda cold starts |
| Top-level await becomes available | Synchronous startup configuration routines become easier to write |
| Ability to adopt the latest libraries | Enables direct use of node-fetch v3.x, chalk v5.x, and similar releases |
| Vite- and Rolldown-based builds | Development server startup is 5 to 10 times faster |
The downside of inaction is also clear: the biggest risk is that "libraries missing security patches will accumulate." This coincides with the major language-level transition covered in the TypeScript 6 Migration Guide, and tackling both at once ultimately proves to be more cost-effective.
Overview of tasks required for the NestJS v12 migration
Here is an audit of tasks in a typical enterprise NestJS project, detailing what needs to be modified.
| Work area | Details | Estimated effort (medium-sized project) |
|---|---|---|
Migrating package.json to "type": "module" | Entry points and extensions | 0.5 person-days |
Updating module and moduleResolution in tsconfig.json | Upgrading to NodeNext and Bundler lines | 0.5 person-days |
Adding .js extensions to relative imports | Modifying all import statements | 1 to 3 person-days (automatable) |
Replacing __dirname and __filename | Migrating to a import.meta.url base | 0.5 person-days |
| Rewriting dynamic require calls | Migrating to a import() base | 1 to 2 person-days |
| Checking decorator and DI compatibility | NestJS metadata generation behavior | 1 to 3 person-days |
| Migrating validators to Standard Schema | class-validator → Zod, etc. | 2 to 5 person-days |
| Updating build, Docker, and CI | Migrating to Vite base | 1 to 3 person-days |
| Testing (Jest / Vitest) adaptation | Switching to ESM mode | 2 to 5 person-days |
| Verification and performance benchmarking | All endpoints | 3 to 5 person-days |
In total, expect approximately 5 to 10 person-days for small projects, 15 to 30 person-days for medium projects, and 30 to 60 person-days for large projects.
Phased migration approach — avoiding an "all-at-once v12 upgrade"
Because upgrading major versions all at once dramatically increases failure rates, we recommend a two-phase migration.
Phase A(v11 系のまま ESM 化)— 4〜6 週間
├─ "type": "module" + .js 拡張子付与
├─ __dirname 置換
├─ ライブラリの ESM 対応版へ更新
└─ テスト・本番リリース → 1〜2 週間運用観察
Phase B(v11 → v12 への移行)— 4〜6 週間
├─ NestJS v12 へ依存更新
├─ Standard Schema 対応
├─ ツールチェーン Vite/Rolldown へ
└─ テスト・本番リリース → 性能比較レポート
By designing the process so that stopping after Phase A still delivers tangible value, issues with library compatibility can be resolved even if Phase B is delayed due to budget constraints. This follows the phased migration mindset outlined in Migrating from Prisma to Drizzle ORM and Migrating to AWS App Runner: in custom development, designs where value is locked in even if the migration pauses midway are exceedingly resilient.
Tasks that can and cannot be automated
Here is an evaluation of automation feasibility to ensure accurate effort estimation.
| Task | Automation | Tool |
|---|---|---|
Adding .js to relative imports | ✅ Possible | ts-add-js-extension / Codemod |
Replacing __dirname | ✅ Possible | Custom jscodeshift |
Updating tsconfig.json | ✅ Possible | Manual + verify with linting |
| Decorator behavior changes | ❌ Manual | Refer to the official NestJS migration guide |
| Migrating validators to schema | △ Semi-automated | Generate initial drafts with LLMs, review by humans |
| ESM adaptation for tests | △ Semi-automated | jest.config for extensionsToTreatAsEsm |
Semi-automation leveraging LLMs is particularly effective for validator migrations, where converting class-validator DTOs into Zod schemas is well within the capabilities of Claude and GPT. However, because generated outputs may not be type-equivalent, comprehensive coverage through integration tests on every endpoint is mandatory.
Implementation sample — minimal diff for Phase A
Here are the representative diffs required in Phase A.
// package.json
{
"type": "module",
"main": "dist/main.js",
"scripts": {
"build": "nest build",
"start": "node --enable-source-maps dist/main.js"
}
}
// tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"esModuleInterop": true
}
}
// src/utils/path.ts — __dirname の代替
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = dirname(__filename);
// src/main.ts — 相対 import に .js 拡張子
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js'; // 拡張子必須
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
With this minimal set of changes, you can reach a state where "the ESM build passes and production runs successfully."
Pitfalls and workarounds
| Pitfall | Symptom | Measure |
|---|---|---|
| Third-party DI lacks ESM support | DI fails to resolve at startup | Bump to an ESM-compatible version or build an in-house wrapper |
| Stumbling blocks around Worker, Bull, or RabbitMQ | Queue workers fail to boot | Adopt a dual configuration running only the worker side on CJS |
| Jest mock behavior changes | Test spies fail to attach | Consider migrating to Vitest simultaneously |
| TypeORM and Prisma behavior | Entity auto-discovery breaks | Resolve paths using absolute paths |
| Docker image bloat | Changes in bundling format lead to duplicate copying | Clean up output: 'standalone' artifacts |
Worker and Bull components are particularly prone to issues; keeping the option to leave worker processes on CJS during Phase A significantly lowers migration risk.
Summary — Turning "ESM migration" into a milestone for client maintenance
The full ESM migration in NestJS v12 represents one of the largest major updates of 2026 for enterprise backend systems. Neglecting it halts dependency updates and drives up CVE remediation costs, making it wise to explicitly place this "inevitable task" onto the roadmap.
In migrating from NestJS v11 to v12, both the required timeline and staffing depend on the balance between what can be handled mechanically with Codemods (adding .js to relative imports, replacing __dirname) and what requires human judgment (decorator behavior changes, migrating validators to Standard Schema). Factors such as whether Worker or Bull setups are present, or whether tests remain on Jest, also alter where to draw the boundary for Phase A. Therefore, we design our approach after reviewing your current tsconfig and dependency tree. If you are looking to "upgrade an API server built with TypeScript and NestJS to the latest version" or are "facing challenges with CVE mitigation due to outdated libraries," please reach out via our inquiry form.









