Node.jsにおけるモジュールシステムの在り方は、ここ数年で劇的な変化を遂げました。
かつて主流であったCommonJS(CJS)から、JavaScriptの標準規格であるES Modules(ESM)への移行は、2026年現在においてほぼ完了したと言えます。
しかし、長年蓄積されたCJSの資産や、特有の動作仕様の違いに悩まされる開発者は少なくありません。
本記事では、Node.jsにおけるESMの現在の立ち位置を整理し、スムーズな移行を実現するための具体的な注意点とベストプラクティスを詳しく解説します。
Node.jsにおけるES Modules(ESM)の現状
2026年現在、Node.jsの最新LTSバージョンにおいて、ESMはデフォルトの選択肢として完全に定着しています。
主要なライブラリの多くが「ESM-only」へと舵を切っており、新しいプロジェクトを開始する際にCJSを選択するメリットは少なくなっています。
Node.js自体も、ESMとCJSの相互運用性を高めるための機能を強化し続けてきました。
以前は困難だったESMからCJSライブラリの読み込みや、その逆のパターンについても、環境整備が進んだことでトラブルは減少しています。
それでもなお、実行コンテキストの違いによるエラーや、ビルドツールの設定ミスによる混乱は、現場のエンジニアにとって大きな課題です。
CommonJSからESMへの移行が進む背景
なぜNode.jsコミュニティは、使い慣れたCJSを捨ててESMへの移行を推進してきたのでしょうか。
最大の理由は、ブラウザ環境とのコード共有を円滑にするためです。
モダンなWebフロントエンド開発ではESMが標準であり、Node.js側もこれに合わせることで、ユニバーサルなJavaScriptコードの記述が可能になります。
また、ESMは静的な解析が容易であるため、不要なコードを削除する「Tree Shaking」の効果を最大限に引き出すことができます。
これにより、最終的なアプリケーションのバンドルサイズを縮小し、実行パフォーマンスを向上させることが期待できます。
さらに、Top-level Awaitのような強力な機能は、ESMでしか利用できない点も大きな魅力です。
ESMを導入するための基本設定
Node.jsプロジェクトでESMを有効にするには、主に2つの方法があります。
package.jsonによる設定
プロジェクト全体をESMとして扱うには、package.jsonに"type": "module"を追記します。
{
"name": "my-esm-project",
"version": "1.0.0",
"type": "module",
"dependencies": {
"lodash-es": "^4.17.21"
}
}
この設定を行うことで、拡張子が.jsのファイルはすべてES Modulesとして解釈されます。
拡張子による使い分け
プロジェクト内でESMとCJSを混在させる必要がある場合は、拡張子を明示的に使い分けます。
| 拡張子 | モジュールシステム | 説明 |
|---|---|---|
.mjs | ES Modules | package.jsonの設定に関わらず、常にESMとして動作します。 |
.cjs | CommonJS | package.jsonの設定に関わらず、常にCJSとして動作します。 |
.js | 設定に依存 | "type": "module"があればESM、なければCJSになります。 |
CommonJSから移行する際の技術的な注意点
CJSからESMへ移行する際、コードを書き換えるだけでは動作しないケースが多々あります。
ここでは、開発者が特につまずきやすいポイントを深掘りします。
__dirname と __filename の不在
CJSで頻繁に利用されていた__dirnameや__filenameといったグローバル変数は、ESMでは定義されていません。
これらを使用しようとすると、ReferenceErrorが発生してプログラムが停止します。
ESMでファイルパスを取得するには、import.meta.urlを利用する必要があります。
// ESMでのファイルパス取得方法
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(`現在のファイルパス: ${__filename}`);
console.log(`現在のディレクトリ: ${__dirname}`);
現在のファイルパス: /Users/user/project/index.js
現在のディレクトリ: /Users/user/project
モジュール解決時の拡張子省略が不可
CJSでは、require('./utils')のようにファイルの拡張子を省略してインポートすることが可能でした。
しかし、ESMの仕様ではインポートパスに拡張子を明示することが厳格に求められます。
たとえ同一ディレクトリ内のJavaScriptファイルであっても、import { func } from './utils.js';と記述しなければなりません。
これはブラウザの動作仕様に合わせたものであり、Node.js独自の「自動補完」に頼らない設計になっています。
JSONファイルのインポート
CJSではrequire('./data.json')で簡単にJSONを読み込めましたが、ESMでは少し手順が異なります。
2026年現在の環境では、Import Attributesを使用してJSONをインポートするのが標準的です。
// JSONファイルをインポートする最新の書き方
import config from './config.json' with { type: 'json' };
console.log(config.appName);
以前のassert { type: 'json' }という構文は、最新の仕様でwithに置き換えられた点に注意してください。
相互運用性(Interoperability)の課題と解決策
現実の開発現場では、自作コードはESMであっても、利用しているライブラリがCJSである場合が少なくありません。
Node.jsにおけるESMとCJSの相互運用の仕組みを正しく理解しておくことが重要です。
ESMからCJSを呼び出す
ESM内では、import文を使用してCJSモジュールを読み込むことができます。
ただし、CJS側がmodule.exportsで単一のオブジェクトをエクスポートしている場合、ESM側では「デフォルトインポート」として扱う必要があります。
// CommonJSモジュールの例 (old-lib.cjs)
module.exports = {
hello: () => console.log('Hello from CJS')
};
// ESMからの呼び出し
import pkg from './old-lib.cjs';
pkg.hello();
名前付きエクスポート(Named Export)のように扱おうとするとエラーになる場合があるため、一度デフォルトインポートで受け取るのが安全です。
CJSからESMを呼び出す
逆のパターンである「CJSからESMを呼び出す」のは、以前は非常に困難でした。
ESMは非同期でロードされる性質を持っているため、同期的なrequireでは読み込めないからです。
現在では、動的なimport()構文を使用することで、CJS内からESMを非同期にロードすることが可能です。
// CJS内でESMを読み込む
async function loadESM() {
const { modernFunc } = await import('./modern-module.js');
modernFunc();
}
loadESM();
最近のNode.jsでは、条件付きでCJSからの同期的なrequire(esm)をサポートする動きもありますが、環境依存が強いため、基本的には非同期インポートを推奨します。
2026年におけるESM運用のベストプラクティス
ESMへの移行を終えた、あるいはこれから進めるプロジェクトにおいて、維持管理性を高めるための指針を紹介します。
TypeScriptとESMの親和性
TypeScriptを使用してESMプロジェクトを構築する場合、tsconfig.jsonの設定が鍵を握ります。
moduleおよびmoduleResolutionに、nodenextまたはnode16以降を指定することが必須です。
これにより、TypeScriptコンパイラはNode.jsのESM解決ルールに従って型チェックを行ってくれます。
また、コンパイル後のJavaScriptでも.js拡張子が必要になるため、TypeScriptのソースコード内でもインポートパスに .js を付けるという独特の記述が必要になります。
テストフレームワークの選択
ESMへの移行で最も苦労するのが、テスト環境の構築です。
かつて主流だったJestは、ESMをネイティブでサポートするために複雑な設定や実験的機能を必要とします。
2026年現在では、ESMを第一級市民として扱っているVitestや、Node.js標準のテストランナー(node:test)の利用が推奨されます。
これらを選択することで、Babelやts-nodeといった重厚な変換層を介さずに、高速かつシンプルにテストを実行できます。
パッケージ公開時の「Dual Package」対応
もしあなたがライブラリ開発者であるなら、ESMとCJSの両方で利用できるパッケージを提供すべきかもしれません。
これを「Dual Package」と呼び、package.jsonのexportsフィールドで制御します。
{
"name": "my-library",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs"
}
}
}
この設定により、利用者がimportを使った場合はESM版が、requireを使った場合はCJS版が自動的に選択されます。
まとめ
Node.jsにおけるES Modulesへの移行は、単なる構文の変更ではなく、JavaScriptエコシステム全体の標準化に向けた重要なステップです。
2026年という現在の状況下では、ESMを基盤とした開発が当たり前となり、以前のような「試行錯誤の時期」は過ぎ去りました。
しかし、__dirnameの代替や拡張子の明示といった、ESM固有のルールを正しく理解していなければ、予期せぬトラブルに時間を奪われることになります。
本記事で紹介した設定方法やベストプラクティスを参考に、モダンなNode.js開発環境を構築してください。
既存のCommonJS資産を尊重しつつも、新しい標準であるES Modulesを積極的に活用することで、より堅牢でスケーラブルなアプリケーションを実現できるはずです。
