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にオブジェクトや関数を代入します。

JavaScript
// mathUtils.cjs
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// 複数の機能を一つのオブジェクトとして公開する
module.exports = {
    add,
    subtract
};

読み込む側では、requireを使用して変数に格納します。

JavaScript
// app.cjs
const { add, subtract } = require('./mathUtils.cjs');

console.log(add(10, 5));
console.log(subtract(10, 5));
実行結果
15
5

ES Modulesの書き方

ESMでは、個別にexportを付ける「名前付きエクスポート」と、ファイルごとに一つだけ定義できる「デフォルトエクスポート」があります。

JavaScript
// 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を使用します。

拡張子を省略できない点に注意が必要です。

JavaScript
// 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.exportsimport / export
読み込みのタイミング実行時(ランタイム)解析時(パースタイム)
実行モードデフォルト常に strict mode
トップレベル Await利用不可利用可能
__dirname / __filename利用可能利用不可(代替手段が必要)

静的解析による最適化

ESMは、コードを実行する前に「どのモジュールが何をエクスポートしているか」を完全に把握できます。

これにより、Tree Shakingと呼ばれる最適化手法が効果的に機能します。

Tree Shakingとは、使用されていないコードをビルドプロセスで自動的に削除する仕組みのことです。

一方、CommonJSは実行中に条件分岐などで動的に読み込みを行うため、静的な最適化が困難です。

トップレベル Await のサポート

ESMの大きな利点の一つは、async関数で囲まなくてもawaitが直接使用できることです。

JavaScript
// 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に設定を追加することです。

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を使用します。

JavaScript
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname);

このように、標準モジュールのurlpathを組み合わせて自前で定義するのが一般的です。

JSONファイルのインポート

以前のNode.jsでは、JSONファイルを直接requireで読み込むことが可能でした。

ESMにおいては、セキュリティ上の理由から「Import Attributes」という仕様を用いる必要があります。

JavaScript
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の内容は「デフォルトエクスポート」として扱われます。

JavaScript
import cjsModule from './legacy-lib.cjs';

ただし、CJS側が名前付きエクスポートのように見せていても、ESM側から分割代入で直接インポートできない場合があるため注意が必要です。

CJSからESMを読み込む

逆に、CommonJSのコードからESMを読み込むのは少し複雑です。

CommonJSのrequire()は同期処理であるため、非同期であるESMを直接読み込むことができません。

そのため、動的インポートであるimport()関数を使用する必要があります。

JavaScript
// 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およびmoduleResolutionNodeNextまたはNode16以降に設定しましょう。

これにより、Node.jsのモジュール解決ルールをTypeScriptが正しく理解し、型安全な開発が可能になります。

ライブラリ公開時の配慮

もしあなたがライブラリを開発し、npmに公開するのであれば、ESM形式での提供は必須です。

可能であれば、古いプロジェクトでも利用できるようにexportsフィールドを活用してDual Packageとして公開するのが最も親切な設計です。

JSON
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

この設定により、利用者の環境に合わせて最適なエントリポイントが自動的に選択されます。

まとめ

Node.jsのモジュールシステムは、かつてのCommonJS時代から、現在のES Modules標準時代へと完全に移行しました。

ESMは、静的解析による最適化、ブラウザとの互換性、トップレベルAwaitのサポートなど、多くの利点を持っています。

一方で、パスの扱いやJSONのインポート方法、CommonJSとの相互運用のルールなど、移行に伴う変更点もいくつか存在します。

これからのNode.js開発においては、「原則ESMを使用し、過去の資産や特定の制約がある場合のみCommonJSを併用する」という方針がベストプラクティスとなります。

最新の仕様を正しく理解し活用することで、より堅牢で効率的なサーバーサイドJavaScriptの開発を実現しましょう。