Node.jsにおける開発環境は、近年大きな転換期を経て現在の形に定着しました。
以前はCommonJS(CJS)が主流でしたが、現在はECMAScript Modules(ESM)が標準として広く採用されています。
これから新しいプロジェクトを立ち上げる際、どちらのモジュールシステムを選択すべきか迷う方も少なくありません。
この記事では、2026年時点での最新状況を踏まえ、Node.jsのモジュールシステムの使い分けと最適な設計手法について詳しく解説します。
Node.jsにおける2つのモジュールシステム
Node.jsには、歴史的な経緯から2つの異なるモジュールシステムが存在しています。
一つは、Node.jsの誕生初期から使われてきたCommonJS(CJS)です。
もう一つは、JavaScriptの言語仕様として標準化されたECMAScript Modules(ESM)です。
かつてのNode.js開発ではCommonJSが当たり前でしたが、現在はエコシステム全体がESMへとシフトしています。
この2つには互換性がない部分があり、正しく理解していないとランタイムエラーやビルドエラーの原因となります。
CommonJS(CJS)の特徴
CommonJSは、サーバーサイドJavaScriptのために設計された最初のモジュール仕様です。
require()関数を使用してモジュールを読み込み、module.exportsを使用して機能を公開します。
最大の特徴は、モジュールの読み込みが「同期」で行われるという点にあります。
これは、ファイルシステムからファイルを読み込む際に実行を一時停止し、完了を待つ仕組みであることを意味します。
ECMAScript Modules(ESM)の特徴
ESMは、Webブラウザとサーバーサイドの両方で動作するように設計された公式の標準仕様です。
import文とexport文を使用し、プログラムの構造を静的に解析できるというメリットがあります。
また、ESMは「非同期」での読み込みを基本としているため、大規模なアプリケーションでの効率的なロードが可能です。
Node.js環境においても、現在はESMがデフォルトの推奨スタイルとなっています。
構文の違いと具体的なコード例
2つのシステムでは、コードの書き方が根本的に異なります。
ここでは、最も基本的な「エクスポート」と「インポート」の書き方を比較してみましょう。
CommonJSの書き方
CommonJSでは、module.exportsにオブジェクトや関数を代入します。
// mathUtils.cjs
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// 複数の機能を一つのオブジェクトとして公開する
module.exports = {
add,
subtract
};
読み込む側では、requireを使用して変数に格納します。
// app.cjs
const { add, subtract } = require('./mathUtils.cjs');
console.log(add(10, 5));
console.log(subtract(10, 5));
15
5
ES Modulesの書き方
ESMでは、個別にexportを付ける「名前付きエクスポート」と、ファイルごとに一つだけ定義できる「デフォルトエクスポート」があります。
// mathUtils.mjs
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// デフォルトエクスポートの例
export default function multiply(a, b) {
return a * b;
}
読み込む側では、importを使用します。
拡張子を省略できない点に注意が必要です。
// app.mjs
import multiply, { add, subtract } from './mathUtils.mjs';
console.log(add(10, 5));
console.log(multiply(10, 5));
15
50
ESMとCJSの主要な相違点
単なる構文の違いだけでなく、動作面においても重要な違いがいくつか存在します。
これらを把握しておくことで、トラブルシューティングがスムーズになります。
| 機能 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| キーワード | require / module.exports | import / export |
| 読み込みのタイミング | 実行時(ランタイム) | 解析時(パースタイム) |
| 実行モード | デフォルト | 常に strict mode |
| トップレベル Await | 利用不可 | 利用可能 |
| __dirname / __filename | 利用可能 | 利用不可(代替手段が必要) |
静的解析による最適化
ESMは、コードを実行する前に「どのモジュールが何をエクスポートしているか」を完全に把握できます。
これにより、Tree Shakingと呼ばれる最適化手法が効果的に機能します。
Tree Shakingとは、使用されていないコードをビルドプロセスで自動的に削除する仕組みのことです。
一方、CommonJSは実行中に条件分岐などで動的に読み込みを行うため、静的な最適化が困難です。
トップレベル Await のサポート
ESMの大きな利点の一つは、async関数で囲まなくてもawaitが直接使用できることです。
// main.mjs
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
CommonJSでは必ずasync関数内に記述する必要があったため、この機能は非同期処理が多い現代の開発において非常に便利です。
Node.jsでESMを有効にする方法
Node.jsでファイルをESMとして認識させるには、主に2つの方法があります。
プロジェクト全体の構成に合わせて適切な方を選んでください。
1. package.json で “type”: “module” を指定する
最も推奨される方法は、package.jsonに設定を追加することです。
{
"name": "my-project",
"version": "1.0.0",
"type": "module"
}
この設定を行うと、そのディレクトリ内にあるすべての.jsファイルがESMとして扱われます。
逆にCommonJSとして動作させたいファイルがある場合は、拡張子を.cjsにする必要があります。
2. 拡張子 .mjs を使用する
package.jsonの設定に関わらず、拡張子が.mjsのファイルは常にESMとして扱われます。
特定のスクリプトだけを最新の構文で書きたい場合に便利です。
同様に、常にCommonJSとして扱われる拡張子は.cjsとなります。
ESM環境での注意点と回避策
CJSからESMに移行する際、多くの開発者が最初に直面する壁がいくつかあります。
特にパス関連の変数が使えない点は、よくあるトラブルの一つです。
__dirname と __filename の欠如
ESM内では、現在のディレクトリパスを示す__dirnameや、ファイルパスを示す__filenameが定義されていません。
これらを使おうとすると ReferenceError が発生します。
ESMでこれらと同等の情報を取得するには、import.meta.urlを使用します。
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
このように、標準モジュールのurlとpathを組み合わせて自前で定義するのが一般的です。
JSONファイルのインポート
以前のNode.jsでは、JSONファイルを直接requireで読み込むことが可能でした。
ESMにおいては、セキュリティ上の理由から「Import Attributes」という仕様を用いる必要があります。
import data from './config.json' with { type: 'json' };
console.log(data.name);
古いNode.jsバージョンではこの構文が異なる場合や、フラグが必要な場合があるため注意してください。
CJSとESMの相互運用(インターオペラビリティ)
現実の開発では、自分のコードをESMで書いていても、ライブラリがCommonJSであるといった状況が多々あります。
Node.jsはこれらを共存させる仕組みを提供していますが、ルールがあります。
ESMからCJSを読み込む
ESMからは、CommonJSモジュールをimport文で読み込むことができます。
この際、module.exportsの内容は「デフォルトエクスポート」として扱われます。
import cjsModule from './legacy-lib.cjs';
ただし、CJS側が名前付きエクスポートのように見せていても、ESM側から分割代入で直接インポートできない場合があるため注意が必要です。
CJSからESMを読み込む
逆に、CommonJSのコードからESMを読み込むのは少し複雑です。
CommonJSのrequire()は同期処理であるため、非同期であるESMを直接読み込むことができません。
そのため、動的インポートであるimport()関数を使用する必要があります。
// CommonJS内での記述
async function loadConfig() {
const { config } = await import('./modern-config.mjs');
return config;
}
この制約があるため、ライブラリ作者は「Pure ESM(ESMのみ提供)」にするか「Dual Package(両方提供)」にするかの選択を迫られます。
2026年における最新ベストプラクティス
現在のNode.jsエコシステムにおいて、最も推奨される開発スタイルをまとめます。
これらに従うことで、メンテナンス性が高く、将来にわたって安全なコードを維持できます。
原則としてESMをデフォルトにする
新規プロジェクトを開始する場合は、迷わずESMを選択してください。
具体的には、package.jsonに"type": "module"を記述します。
主要なライブラリの多くが既にESMファーストに移行しており、CommonJSを使い続けるメリットは年々減少しています。
TypeScriptとの組み合わせ
TypeScriptを使用する場合も、出力ターゲットをESMに設定することが推奨されます。
tsconfig.jsonの設定でmoduleおよびmoduleResolutionをNodeNextまたはNode16以降に設定しましょう。
これにより、Node.jsのモジュール解決ルールをTypeScriptが正しく理解し、型安全な開発が可能になります。
ライブラリ公開時の配慮
もしあなたがライブラリを開発し、npmに公開するのであれば、ESM形式での提供は必須です。
可能であれば、古いプロジェクトでも利用できるようにexportsフィールドを活用してDual Packageとして公開するのが最も親切な設計です。
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
この設定により、利用者の環境に合わせて最適なエントリポイントが自動的に選択されます。
まとめ
Node.jsのモジュールシステムは、かつてのCommonJS時代から、現在のES Modules標準時代へと完全に移行しました。
ESMは、静的解析による最適化、ブラウザとの互換性、トップレベルAwaitのサポートなど、多くの利点を持っています。
一方で、パスの扱いやJSONのインポート方法、CommonJSとの相互運用のルールなど、移行に伴う変更点もいくつか存在します。
これからのNode.js開発においては、「原則ESMを使用し、過去の資産や特定の制約がある場合のみCommonJSを併用する」という方針がベストプラクティスとなります。
最新の仕様を正しく理解し活用することで、より堅牢で効率的なサーバーサイドJavaScriptの開発を実現しましょう。
