はじめに

Node.jsにはCommonJSと**ES Modules(ESM)**という2つのモジュールシステムが存在します。歴史的にNode.jsはCommonJSを採用してきましたが、ECMAScript標準としてES Modulesが策定されて以降、Node.jsでもESMのサポートが進んでいます。

本記事では、以下の内容を解説します。

  • CommonJSとES Modulesの基本構文
  • package.jsontypeフィールドによるモジュール形式の指定
  • 拡張子.mjs.cjsの使い分け
  • 2つのモジュールシステム間の相互運用性
  • プロジェクトに適したモジュール形式の選択基準

この記事を読み終えると、Node.jsプロジェクトで適切なモジュール形式を選択し、実装できるようになります。

実行環境

本記事のコードは以下の環境で動作確認しています。

項目 バージョン
Node.js 20.x LTS以上
npm 10.x以上
OS Windows/macOS/Linux

CommonJSの基本

CommonJSはNode.jsで最初から採用されているモジュールシステムです。require()関数でモジュールを読み込み、module.exportsまたはexportsでエクスポートします。

CommonJSの構文

CommonJSでは、モジュールを読み込む際にrequire()関数を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// mathUtils.js(CommonJS形式)

// 関数を定義
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

// module.exportsでエクスポート
module.exports = {
  add,
  subtract,
};

このモジュールを使用する側は、require()で読み込みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// main.js(CommonJS形式)

// モジュールの読み込み
const mathUtils = require('./mathUtils');

// 関数の使用
console.log(mathUtils.add(2, 3));      // 5
console.log(mathUtils.subtract(5, 2)); // 3

// 分割代入で個別に取得することも可能
const { add, subtract } = require('./mathUtils');
console.log(add(10, 5)); // 15

exportsショートカット

module.exportsへの参照としてexportsオブジェクトも使用できます。ただし、exportsを直接再代入すると正しく動作しません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 正しい使い方
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

// 誤った使い方(動作しない)
exports = {
  add: (a, b) => a + b,
};
// この場合、module.exportsとの参照が切れるため、
// require()しても空オブジェクトが返される

CommonJSの特徴

CommonJSには以下の特徴があります。

特徴 説明
同期的な読み込み require()はファイルを同期的に読み込む
動的な読み込み 条件分岐やループ内でもrequire()を呼び出せる
キャッシング 一度読み込んだモジュールはキャッシュされる
__filename__dirname 現在のファイルパスとディレクトリパスが利用可能
1
2
3
4
5
6
// 動的な読み込みの例
const moduleName = process.env.NODE_ENV === 'production'
  ? './prodConfig'
  : './devConfig';

const config = require(moduleName);

ES Modulesの基本

ES Modules(ESM)はECMAScript 2015(ES6)で標準化されたモジュールシステムです。importexportキーワードを使用します。

ES Modulesの構文

ES Modulesでは、exportキーワードでエクスポートし、importキーワードでインポートします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// mathUtils.mjs(ES Modules形式)

// 名前付きエクスポート
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// 定数のエクスポート
export const PI = 3.14159;

インポート側では、import文を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// main.mjs(ES Modules形式)

// 名前付きインポート
import { add, subtract, PI } from './mathUtils.mjs';

console.log(add(2, 3));      // 5
console.log(subtract(5, 2)); // 3
console.log(PI);             // 3.14159

// 別名でインポート
import { add as sum } from './mathUtils.mjs';
console.log(sum(10, 20)); // 30

// すべてをまとめてインポート
import * as math from './mathUtils.mjs';
console.log(math.add(1, 2)); // 3

デフォルトエクスポート

モジュールのメイン機能を1つだけエクスポートする場合は、デフォルトエクスポートを使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Calculator.mjs

export default class Calculator {
  add(a, b) {
    return a + b;
  }

  subtract(a, b) {
    return a - b;
  }
}
1
2
3
4
5
6
7
// main.mjs

// デフォルトインポート(任意の名前を付けられる)
import Calculator from './Calculator.mjs';

const calc = new Calculator();
console.log(calc.add(5, 3)); // 8

ES Modulesの特徴

ES Modulesには以下の特徴があります。

特徴 説明
静的な構造 インポート/エクスポートはファイルのトップレベルに記述
非同期読み込み 動的import()はPromiseを返す
Strict mode 自動的にStrict modeで実行される
import.meta モジュールのメタ情報(URL等)にアクセス可能
1
2
3
// 動的インポート(ES Modules)
const moduleName = './dynamicModule.mjs';
const module = await import(moduleName);

package.jsonのtypeフィールド

Node.jsはpackage.jsontypeフィールドを参照して、.jsファイルをどのモジュールシステムとして解釈するかを決定します。

typeフィールドの設定

1
2
3
4
5
{
  "name": "my-esm-project",
  "version": "1.0.0",
  "type": "module"
}

typeフィールドには以下の値を設定できます。

説明
"module" .jsファイルをES Modulesとして解釈
"commonjs" .jsファイルをCommonJSとして解釈(デフォルト)
未指定 CommonJSとして解釈(後方互換性のため)

モジュール形式の判定フロー

Node.jsは以下の順序でモジュール形式を判定します。

flowchart TD
    A[ファイルを読み込む] --> B{拡張子は?}
    B -->|.mjs| C[ES Modulesとして実行]
    B -->|.cjs| D[CommonJSとして実行]
    B -->|.js| E{package.jsonのtypeは?}
    E -->|"module"| C
    E -->|"commonjs"または未指定| D

推奨設定

新規プロジェクトでは、以下のように明示的にtypeフィールドを設定することを推奨します。

1
2
3
4
5
6
7
8
{
  "name": "modern-nodejs-project",
  "version": "1.0.0",
  "type": "module",
  "engines": {
    "node": ">=20.0.0"
  }
}

拡張子.mjsと.cjsの使い分け

package.jsontypeフィールドに関わらず、拡張子によってモジュール形式を明示できます。

拡張子ごとの挙動

拡張子 モジュール形式 用途
.mjs ES Modules CommonJSプロジェクト内でESMを使いたい場合
.cjs CommonJS ESMプロジェクト内でCommonJSを使いたい場合
.js typeフィールドに依存 プロジェクトのデフォルト形式に従う

混在させる場合の例

ESMプロジェクト内でCommonJS形式の設定ファイルを使用する例を示します。

1
2
3
4
{
  "name": "esm-project",
  "type": "module"
}
1
2
3
4
5
// config.cjs(CommonJS形式で記述)
module.exports = {
  port: 3000,
  host: 'localhost',
};
1
2
3
4
5
6
7
8
// server.js(ES Modules形式)
import { createRequire } from 'node:module';

// ES Modules内でrequireを使用するためのヘルパー
const require = createRequire(import.meta.url);
const config = require('./config.cjs');

console.log(config.port); // 3000

CommonJSとES Modulesの相互運用

2つのモジュールシステム間での相互運用には、いくつかの制約と注意点があります。

ES ModulesからCommonJSを読み込む

ES ModulesからCommonJSモジュールをimportすることは可能です。

1
2
3
4
5
// legacy.cjs(CommonJS)
module.exports = {
  greet: (name) => `Hello, ${name}!`,
  version: '1.0.0',
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// main.mjs(ES Modules)
// デフォルトインポートでmodule.exportsを取得
import legacy from './legacy.cjs';

console.log(legacy.greet('World')); // Hello, World!
console.log(legacy.version);        // 1.0.0

// 名前付きインポートも可能(静的解析による推測)
import { greet } from './legacy.cjs';
console.log(greet('Node.js')); // Hello, Node.js!

CommonJSからES Modulesを読み込む

Node.js 20以降では、CommonJSから同期的なES Modulesをrequire()で読み込むことが可能になりました(トップレベルawaitを含まない場合)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// modern.mjs(ES Modules、トップレベルawaitなし)
export function calculate(x) {
  return x * 2;
}

export default class Calculator {
  multiply(a, b) {
    return a * b;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// main.cjs(CommonJS)
// ES Modulesをrequireで読み込む(Node.js 20+)
const modern = require('./modern.mjs');

console.log(modern);
// [Module: null prototype] {
//   calculate: [Function: calculate],
//   default: [class Calculator]
// }

console.log(modern.calculate(5)); // 10

const Calculator = modern.default;
const calc = new Calculator();
console.log(calc.multiply(3, 4)); // 12

ただし、トップレベルawaitを含むESMはrequire()で読み込めません。その場合は動的import()を使用します。

1
2
3
// asyncModule.mjs(トップレベルawaitを含む)
const data = await fetch('https://api.example.com/data');
export const result = await data.json();
1
2
3
4
5
6
7
8
// main.cjs(CommonJS)
// 非同期ESMは動的import()で読み込む
async function main() {
  const { result } = await import('./asyncModule.mjs');
  console.log(result);
}

main();

相互運用時の注意点

相互運用時には以下の点に注意が必要です。

項目 注意点
__dirname/__filename ES Modulesでは使用不可。import.meta.dirname/import.meta.filenameを使用
require.resolve ES Modulesではimport.meta.resolve()を使用
JSONインポート ES Modulesではwith { type: 'json' }が必要
拡張子の省略 ES Modulesでは拡張子の指定が必須
1
2
3
4
5
6
7
// ES ModulesでのJSONインポート
import packageJson from './package.json' with { type: 'json' };
console.log(packageJson.name);

// ES Modulesでの__dirnameの代替
console.log(import.meta.dirname);  // /path/to/directory
console.log(import.meta.filename); // /path/to/file.mjs

どちらのモジュール形式を選ぶべきか

プロジェクトの特性に応じて、適切なモジュール形式を選択しましょう。

ES Modulesを選ぶべきケース

  • 新規プロジェクトを開始する場合
  • モダンなJavaScript構文を積極的に使用したい場合
  • ブラウザとNode.jsで同じコードを共有したい場合
  • Tree shakingなどのビルド最適化を活用したい場合

CommonJSを維持すべきケース

  • 既存のCommonJSプロジェクトを保守する場合
  • CommonJS専用の依存パッケージが多い場合
  • レガシー環境との互換性が必要な場合

判断フローチャート

flowchart TD
    A[プロジェクトを開始] --> B{新規プロジェクト?}
    B -->|はい| C{ESMに対応していない<br>依存パッケージがある?}
    C -->|はい| D[CommonJSを選択]
    C -->|いいえ| E[ES Modulesを選択]
    B -->|いいえ| F{大規模な改修が可能?}
    F -->|はい| G{ESM移行のメリットが大きい?}
    G -->|はい| E
    G -->|いいえ| D
    F -->|いいえ| D

実践的な設定例

実際のプロジェクトで使用する設定例を示します。

ES Modulesプロジェクトの推奨設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "name": "esm-project",
  "version": "1.0.0",
  "type": "module",
  "engines": {
    "node": ">=20.0.0"
  },
  "exports": {
    ".": {
      "import": "./src/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "scripts": {
    "start": "node src/index.js",
    "build": "tsc"
  }
}

デュアルパッケージ(ESMとCommonJS両対応)

ライブラリを公開する場合、両方の形式に対応することが求められる場合があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "name": "dual-package-library",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

まとめ

本記事では、Node.jsの2つのモジュールシステムについて解説しました。

  • CommonJSrequire()/module.exportsを使用する従来のモジュールシステム
  • ES Modulesimport/exportを使用するECMAScript標準のモジュールシステム
  • package.jsontypeフィールドで.jsファイルの解釈を制御
  • 拡張子.mjs/.cjsでファイル単位でモジュール形式を明示可能
  • 新規プロジェクトではES Modulesの採用を推奨
  • 相互運用時は各システムの制約に注意

モジュールシステムを正しく理解することで、Node.jsプロジェクトの構成を適切に設計できるようになります。

参考リンク