Node.jsの開発において、モジュールシステムはアプリケーションの構造を決定づける極めて重要な要素です。
長年にわたり、Node.jsではCommonJS様式のrequire関数が中心的な役割を果たしてきました。
しかし、近年のJavaScriptエコシステムでは、標準仕様であるES Modules (ESM) への移行が急速に進んでいます。
2026年現在、新規プロジェクトの多くはESMを採用していますが、既存の膨大なライブラリ資産を活用するためには、requireの深い理解が依然として欠かせません。
本記事では、requireの内部メカニズムから、ESMへのスムーズな移行を実現するための具体的なプラクティスまでを詳しく解説します。
CommonJSとrequire関数の基本構造
Node.jsにおけるCommonJSは、サーバーサイドでJavaScriptを動作させるために設計されたモジュールシステムです。
その中心にあるのがrequire関数であり、外部ファイルのコードを読み込み、オブジェクトとして利用可能にします。
requireは同期的にモジュールをロードするという特徴を持っています。
これは、ファイルが読み込まれるまで次の行のコードが実行されないことを意味します。
まず、簡単なrequireの使用例を見てみましょう。
// math.js
function add(a, b) {
return a + b;
}
// 外部から利用できるように公開する
module.exports = {
add
};
// app.js
// requireを使用してモジュールを読み込む
const math = require('./math.js');
const result = math.add(5, 3);
console.log(`結果は: ${result}`);
結果は: 8
上記の例では、module.exportsに代入されたオブジェクトが、requireの戻り値として渡されます。
このシンプルさが、Node.jsの急速な普及を支えた一因でもあります。
requireの5つの内部プロセス
Node.jsがrequireを呼び出す際、内部では以下の5つのステップが実行されています。
- Resolving (解決): 指定されたパスからファイルの絶対パスを特定します。
- Loading (読み込み): ファイルの内容をメモリ上にロードします。
- Wrapping (ラップ): ロードされたコードを関数で包み、スコープを分離します。
- Evaluating (評価): ラップされた関数を実行し、
exportsオブジェクトを生成します。 - Caching (キャッシュ): 生成されたオブジェクトをメモリに保存し、次回以降の呼び出しを高速化します。
特に重要なのはWrapping (ラップ)の工程です。
Node.jsはファイルを読み込む際、自動的に以下のような関数でコードを囲い込みます。
(function(exports, require, module, __filename, __dirname) {
// あなたが書いたコードがここに配置される
});
これにより、モジュール内で定義した変数がグローバルスコープを汚染することを防いでいます。
また、__dirnameや__filenameといった変数がモジュール内で直接利用できるのは、このラップ処理によって引数として渡されているからです。
requireにおけるモジュール解決アルゴリズム
requireに渡される文字列がどのようにファイルとして特定されるのかを知ることは、トラブルシューティングにおいて非常に有用です。
Node.jsは、「コアモジュール」「ファイル・ディレクトリ」「node_modules」の順に探索を行います。
コアモジュールの優先
fsやpath、httpといったNode.js標準のコアモジュールは、常に最優先で解決されます。
仮にプロジェクト内に同名のファイルを作成しても、コアモジュールが優先的に読み込まれる仕組みになっています。
ファイルおよびディレクトリの探索
パスが /、./、または ../ で始まる場合、Node.jsはそれを相対パスまたは絶対パスとして解釈します。
まず、指定された名前のファイルを直接探します。
ファイルが見つからない場合、.js、.json、.node の順に拡張子を補完して再試行します。
ファイルが存在せずディレクトリが存在する場合、そのディレクトリ内の package.json を確認します。
package.json の main フィールドに記述されたエントリポイントを読み込みます。
main フィールドが存在しない場合は、デフォルトで index.js をロードしようと試みます。
node_modulesの再帰的探索
パスが識別子 (例: lodash) で始まる場合、Node.jsはカレントディレクトリの node_modules を探します。
そこに見つからない場合、親ディレクトリの node_modules を順次遡って探索を続けます。
この仕組みにより、依存関係の階層構造が維持されます。
キャッシュ機構と注意点
requireは非常に強力なキャッシュ機構を備えています。
一度読み込まれたモジュールは require.cache オブジェクトに保存されます。
同一プロセス内で再度同じファイルを require しても、ファイルの読み込みや実行は行われず、キャッシュされたオブジェクトが返されます。
// counter.js
console.log('モジュールが実行されました');
module.exports = { count: 0 };
// main.js
const c1 = require('./counter.js');
const c2 = require('./counter.js');
c1.count = 10;
console.log(`c2のカウント: ${c2.count}`);
モジュールが実行されました
c2のカウント: 10
この結果からわかるように、モジュールの実行は一度きりであり、状態は共有されます。
これはパフォーマンス向上に寄与しますが、シングルトンパターンのような挙動を意図しない場合には注意が必要です。
もし強制的に再読み込みを行いたい場合は、require.cache から該当するキーを手動で削除する必要がありますが、これは推奨されないケースが多いです。
ES Modules (ESM) への移行が必要な理由
2026年の現代において、CommonJSからES Modulesへの移行は避けて通れない課題となっています。
ESMはJavaScriptの公式な標準仕様であり、ブラウザとサーバーの両方で共通のコードを利用できる大きなメリットがあります。
主な移行のメリットは以下の通りです。
- 静的解析の容易さ: インポート・エクスポートが静的に行われるため、IDEの補完や型チェックがより正確になります。
- Tree Shaking (ツリーシェイキング): 使用されていないコードをビルド時に削除しやすくなり、バンドルサイズを削減できます。
- 非同期ロード: ESMは非同期に読み込まれるため、ネットワーク越しにファイルをロードするブラウザ環境と親和性が高いです。
- Top-level await:
async関数で囲うことなく、モジュールの最上位レベルでawaitを使用できます。
特に Top-level await は、データベースの接続待ちや設定ファイルの読み込みにおいてコードを劇的に簡潔にします。
// ESMでのTop-level awaitの例
const connection = await db.connect();
export default connection;
CommonJSでは、このような処理を行うために即時実行関数 (IIFE) や複雑な初期化ロジックが必要でした。
ESMへの移行に向けたベストプラクティス
既存のプロジェクトをESMに移行する際は、段階的なアプローチが推奨されます。
package.jsonの設定変更
最も簡単な移行方法は、package.json に "type": "module" を追加することです。
これにより、プロジェクト内のすべての .js ファイルがESMとして扱われるようになります。
{
"name": "my-project",
"version": "1.0.0",
"type": "module"
}
この設定を行った場合、従来の require は使用できなくなり、import を使用する必要があります。
もし一部のファイルをCommonJSのまま残したい場合は、そのファイルの拡張子を .cjs に変更します。
逆に、プロジェクト全体がCommonJSであっても、特定のファイルだけをESMにしたい場合は .mjs 拡張子を使用します。
__dirname と __filename の代替
ESM環境では、CommonJSで利用可能だった __dirname や __filename が存在しません。
これらを取得するには、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 モジュールの組み合わせが標準的です。
拡張子の明示的指定
CommonJSでは require('./utils') のように拡張子を省略できましたが、ESMでは拡張子の指定が必須です。
import { util } from './utils.js'; のように、ファイル名を完全な形で記述しなければなりません。
これはブラウザ標準の動作に合わせた仕様であり、Node.js環境でもこれを遵守する必要があります。
CommonJSとESMの相互運用 (Interoperability)
現実的なプロジェクトでは、ESMからCommonJSライブラリを呼び出す機会が頻繁にあります。
Node.jsでは、ESMからCommonJSを import することは可能ですが、その逆 (CommonJSからESMを require) はできません。
ESMからCommonJSを読み込む
ESM内では、import 文を使用して .cjs ファイルやCommonJSパッケージを読み込めます。
import pkg from './legacy-module.cjs';
console.log(pkg.message);
多くの場合、CommonJSの module.exports はESMの default export として扱われます。
動的importによる非同期連携
CommonJSモジュール内からESMを読み込みたい場合は、require ではなく、動的import を使用する必要があります。
// CommonJS環境 (app.cjs)
async function loadESM() {
const { someFunction } = await import('./modern-module.mjs');
someFunction();
}
loadESM();
import() はプロミスを返すため、CommonJSの同期的な文脈の中でESMを扱うための唯一の手段となります。
createRequireの活用
ESM環境にいながら、どうしても require の挙動が必要な場合があります (例: JSONファイルの直接読み込みや、特定のレガシーな解決アルゴリズムの利用)。
その場合は、module モジュールの createRequire を使用します。
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// JSONファイルの読み込み
const data = require('./data.json');
これにより、ESM内でも require 関数を疑似的に定義して利用することが可能になります。
移行時に直面する一般的な課題と解決策
移行作業中には、いくつかの技術的な障壁が発生します。
循環参照の違い
CommonJSとESMでは、循環参照 (モジュールがお互いを参照し合う状態) の処理方法が異なります。
CommonJSでは、不完全にロードされたオブジェクトが返されることがあり、実行時エラーや undefined の原因になります。
ESMではバインディングが静的に解決されるため、CommonJSよりも安全に処理されますが、設計自体を見直して循環参照を避けるのが最善です。
デフォルトエクスポートの扱いの差
CommonJSの module.exports = ... をESMから読み込む際、import defaultExport from '...' で受け取ることが一般的です。
しかし、ライブラリによっては named export と default export が混在しており、意図したプロパティにアクセスできないことがあります。
その場合は、import * as all from '...' で中身をすべて確認し、適切なキーを特定してください。
テストツールの対応
JestやMochaといったテストツールもESM対応を進めていますが、以前のCommonJS環境ほど「設定なしで動く」状態ではない場合があります。
2026年時点では vitest のように最初からESMを前提としたモダンなテスティングフレームワークを採用することが、移行の摩擦を減らす近道です。
まとめ
Node.jsにおける require は、長らくエコシステムの中核を担ってきました。
その仕組みを理解することは、モジュールの解決プロセスやキャッシュの振る舞いを把握する上で非常に重要です。
一方で、現在はES Modulesへの完全移行が強く推奨される時代となっています。
新規の開発では "type": "module" を基本とし、既存プロジェクトでは .mjs や .cjs を使い分けながら、段階的に標準仕様へと寄せていくことが求められます。
import.meta.url や createRequire といった移行期のツールを適切に使いこなし、将来にわたって保守性の高いコードベースを構築していきましょう。
本記事が、皆様のNode.js開発におけるモジュール管理とESM移行の一助となれば幸いです。
