T-CREATOR

Node.jsのモジュールシステムの基本:CommonJS と ESM の違い

Node.jsのモジュールシステムの基本:CommonJS と ESM の違い

Node.js でのアプリケーション開発において、「モジュール」という概念は避けて通れない重要な要素です。コードを機能別に分割し、再利用可能な形で管理することは、保守性の高いアプリケーションを構築するための基盤となります。

しかし、Node.js には現在 2 つの主要なモジュールシステムが存在し、多くの開発者が「どちらを使うべきか」「どのような違いがあるのか」と迷われているのではないでしょうか。それが CommonJSESM(ES Modules) です。

この記事では、それぞれのモジュールシステムの特徴から実際の使い分け方法まで、現代の Node.js 開発で必要となる知識を体系的に解説いたします。適切なモジュールシステムを選択することで、より効率的で未来指向の開発が可能になります。

モジュールシステムとは何か

モジュールシステムは、JavaScript における「コードの組織化」と「再利用性の向上」を実現するための仕組みです。大規模なアプリケーション開発において欠かせない概念となっています。

JavaScript におけるモジュールの概念

従来の JavaScript では、すべてのコードが同一のグローバルスコープに配置されていました。これにより、以下のような問題が発生していたのです:

グローバル名前空間の汚染

javascript// 従来のスクリプトベースの問題例
// file1.js
var userName = '田中';
function getUserInfo() {
  return userName;
}

// file2.js
var userName = '佐藤'; // 同名変数により競合が発生
function getUserInfo() {
  // 同名関数により上書き
  return '管理者: ' + userName;
}

このような問題を解決するために、モジュールシステムが導入されました。モジュールシステムでは、以下の重要な機能を提供します:

#機能説明メリット
1スコープ分離モジュール内の変数は外部から直接参照不可名前空間の競合を回避
2明示的なエクスポート外部に公開する要素を明確に定義API の意図を明確化
3依存関係管理必要なモジュールを明示的にインポートコードの依存関係を可視化
4遅延読み込み必要な時点でモジュールを読み込みパフォーマンスの最適化

モジュール化による改善例

javascript// userModule.js - モジュールとして分離
const userName = '田中';

function getUserInfo() {
  return userName;
}

function getFormattedUserInfo(prefix = '') {
  return `${prefix}${getUserInfo()}`;
}

// 外部に公開する関数のみエクスポート
module.exports = {
  getUserInfo,
  getFormattedUserInfo,
};

// main.js - 必要な機能のみインポート
const { getUserInfo } = require('./userModule');
console.log(getUserInfo()); // "田中"

コードの分割・再利用の必要性

モダンな Web 開発では、アプリケーションの複雑さが飛躍的に増大しています。この複雑さに対処するために、コードの分割と再利用が不可欠になっています。

分割の利点

  1. 保守性の向上

    • 機能別にファイルを分けることで、バグの特定と修正が容易
    • 新機能追加時の影響範囲を限定可能
  2. 開発効率の向上

    • 複数人での並行開発が可能
    • 機能単位でのテストが簡単
  3. コードの再利用

    • 一度作成した機能を別のプロジェクトでも活用
    • 共通ライブラリとしての展開が容易

実際のプロジェクト構造例

bashproject/
├── src/
│   ├── utils/
│   │   ├── dateFormatter.js    # 日付処理ユーティリティ
│   │   ├── validation.js       # 入力値検証
│   │   └── logger.js           # ログ出力
│   ├── services/
│   │   ├── userService.js      # ユーザー関連ビジネスロジック
│   │   ├── authService.js      # 認証処理
│   │   └── dataService.js      # データアクセス層
│   ├── controllers/
│   │   ├── userController.js   # ユーザーAPI
│   │   └── adminController.js  # 管理者API
│   └── app.js                  # アプリケーションエントリーポイント
└── package.json

この構造により、各モジュールは明確な責任を持ち、必要に応じて他のモジュールと連携します。

CommonJS の基本概念

CommonJS は、Node.js で長年使用されてきた伝統的なモジュールシステムです。サーバーサイド JavaScript の標準として確立され、現在でも多くのプロジェクトで使用されています。

require() と module.exports の仕組み

CommonJS の中心となるのは、require() 関数と module.exports オブジェクトです。

基本的なエクスポート方法

javascript// mathUtils.js - 数学系ユーティリティモジュール
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

function calculate(operation, a, b) {
  switch (operation) {
    case 'add':
      return add(a, b);
    case 'multiply':
      return multiply(a, b);
    default:
      throw new Error(`未対応の演算: ${operation}`);
  }
}

// 方法1: オブジェクトとしてエクスポート
module.exports = {
  add,
  multiply,
  calculate,
};

// 方法2: 個別にエクスポート
// module.exports.add = add;
// module.exports.multiply = multiply;

// 方法3: 関数そのものをエクスポート(単一エクスポート)
// module.exports = calculate;

基本的なインポート方法

javascript// calculator.js - mathUtilsを使用するモジュール

// 方法1: 全体をインポート
const mathUtils = require('./mathUtils');
console.log(mathUtils.add(5, 3)); // 8

// 方法2: 分割代入でインポート
const { add, multiply } = require('./mathUtils');
console.log(add(5, 3)); // 8
console.log(multiply(4, 6)); // 24

// 方法3: 個別インポート(エイリアス使用)
const { calculate: calc } = require('./mathUtils');
console.log(calc('add', 10, 20)); // 30

動的なインポート CommonJS の大きな特徴の一つは、実行時の動的なモジュール読み込みです:

javascript// 条件に応じたモジュールの動的読み込み
function loadProcessor(type) {
  let processor;

  if (type === 'image') {
    processor = require('./imageProcessor');
  } else if (type === 'text') {
    processor = require('./textProcessor');
  } else {
    processor = require('./defaultProcessor');
  }

  return processor;
}

// 設定に基づいた動的読み込み
const config = require('./config.json');
const database = require(`./db/${config.database.type}`);

Node.js での標準的な使用方法

Node.js では、CommonJS が標準のモジュールシステムとして長年使用されてきました。

ファイルシステム操作の例

javascript// fileManager.js - ファイル操作ユーティリティ
const fs = require('fs');
const path = require('path');

class FileManager {
  constructor(baseDir = './data') {
    this.baseDir = baseDir;
    this.ensureDirectoryExists();
  }

  ensureDirectoryExists() {
    if (!fs.existsSync(this.baseDir)) {
      fs.mkdirSync(this.baseDir, { recursive: true });
    }
  }

  async readFile(filename) {
    try {
      const filePath = path.join(this.baseDir, filename);
      const content = await fs.promises.readFile(
        filePath,
        'utf8'
      );
      return content;
    } catch (error) {
      console.error(
        `ファイル読み込みエラー: ${filename}`,
        error
      );
      throw error;
    }
  }

  async writeFile(filename, content) {
    try {
      const filePath = path.join(this.baseDir, filename);
      await fs.promises.writeFile(
        filePath,
        content,
        'utf8'
      );
      console.log(`ファイル保存完了: ${filename}`);
    } catch (error) {
      console.error(
        `ファイル保存エラー: ${filename}`,
        error
      );
      throw error;
    }
  }

  listFiles() {
    try {
      return fs.readdirSync(this.baseDir);
    } catch (error) {
      console.error('ディレクトリ読み込みエラー', error);
      return [];
    }
  }
}

module.exports = FileManager;

Express.js での活用例

javascript// routes/users.js - ユーザー関連ルート
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { validateUser } = require('../utils/validation');

const router = express.Router();

// ユーザー登録
router.post('/register', async (req, res) => {
  try {
    const { error } = validateUser(req.body);
    if (error) {
      return res
        .status(400)
        .json({ error: error.details[0].message });
    }

    const { email, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);

    const user = new User({
      email,
      password: hashedPassword,
    });
    await user.save();

    res
      .status(201)
      .json({ message: 'ユーザー登録が完了しました' });
  } catch (error) {
    res
      .status(500)
      .json({ error: 'サーバーエラーが発生しました' });
  }
});

// ユーザーログイン
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ email });

    if (
      !user ||
      !(await bcrypt.compare(password, user.password))
    ) {
      return res
        .status(401)
        .json({ error: '認証に失敗しました' });
    }

    const token = jwt.sign(
      { userId: user._id },
      process.env.JWT_SECRET
    );
    res.json({
      token,
      user: { id: user._id, email: user.email },
    });
  } catch (error) {
    res
      .status(500)
      .json({ error: 'サーバーエラーが発生しました' });
  }
});

module.exports = router;

ESM(ES Modules)の登場背景

ESM(ES Modules)は、ECMAScript 2015(ES6)で標準化された、JavaScript 言語仕様レベルでのモジュールシステムです。ブラウザとサーバーサイドの両方で統一的に使用できる点が大きな特徴です。

ECMAScript 標準としての位置づけ

ESM が登場する以前、JavaScript には言語仕様レベルでのモジュールシステムが存在しませんでした。そのため、様々な独自実装が生まれていました:

#モジュールシステム開発元/標準化団体主な使用場面特徴
1CommonJSNode.jsサーバーサイド同期的読み込み
2AMDRequireJSブラウザ非同期読み込み
3UMDコミュニティユニバーサル複数環境対応
4ESMECMAScript標準仕様言語レベルでの標準化

ESM 標準化の意義

ESM の標準化により、以下のメリットが実現されました:

  1. 統一された仕様

    • ブラウザとサーバーサイドで同一の構文
    • 学習コストの削減と知識の移転可能性
  2. 静的解析の向上

    • バンドラーによる最適化が容易
    • Tree shaking(不要コードの除去)が効率的
  3. 将来への投資

    • 言語仕様として長期的にサポート
    • 新機能の追加も標準化プロセスに沿って実施

Node.js での ESM サポート状況

javascript// package.json での設定
{
  "name": "esm-example",
  "version": "1.0.0",
  "type": "module",  // ESM を標準として使用
  "scripts": {
    "start": "node src/app.js"
  }
}

ブラウザとの互換性向上

ESM の最大の利点の一つは、ブラウザとの互換性です。現代のブラウザは ESM をネイティブサポートしており、同じコードをサーバーサイドとクライアントサイドで共有できます。

ブラウザでの ESM 使用例

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>ESM ブラウザテスト</title>
  </head>
  <body>
    <h1>ES Modules テスト</h1>
    <div id="result"></div>

    <!-- ESM を直接ブラウザで読み込み -->
    <script type="module">
      import {
        formatDate,
        addDays,
      } from './utils/dateUtils.js';
      import UserManager from './modules/UserManager.js';

      const userManager = new UserManager();
      const today = new Date();
      const nextWeek = addDays(today, 7);

      document.getElementById('result').innerHTML = `
            <p>今日: ${formatDate(today)}</p>
            <p>来週: ${formatDate(nextWeek)}</p>
            <p>ユーザー数: ${userManager.getUserCount()}</p>
        `;
    </script>
  </body>
</html>

サーバーサイドとの共有モジュール例

javascript// utils/dateUtils.js - サーバーとブラウザで共通使用
export function formatDate(date) {
  return new Intl.DateTimeFormat('ja-JP', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(date);
}

export function addDays(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

export function isWeekend(date) {
  const day = date.getDay();
  return day === 0 || day === 6; // 日曜日または土曜日
}

// Node.js でも同じコードが動作
// import { formatDate, addDays } from './utils/dateUtils.js';

モダンな Web 開発での活用

javascript// frontend/src/api/client.js - APIクライアント
export class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async get(endpoint) {
    const response = await fetch(
      `${this.baseURL}${endpoint}`
    );
    if (!response.ok) {
      throw new Error(
        `HTTP error! status: ${response.status}`
      );
    }
    return response.json();
  }

  async post(endpoint, data) {
    const response = await fetch(
      `${this.baseURL}${endpoint}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      }
    );
    return response.json();
  }
}

// backend/src/utils/apiClient.js - 同じクラスをサーバーでも使用
// (fetch の代わりに node-fetch や undici を使用)
export { ApiClient } from '../../frontend/src/api/client.js';

CommonJS と ESM の技術的差異

CommonJS と ESM には、単なる構文の違いを超えた根本的な技術的差異があります。これらの違いを理解することで、適切な選択ができるようになります。

構文と API の違い

最も分かりやすい違いは、モジュールの読み込みと出力の構文です。

エクスポート構文の比較

javascript// CommonJS のエクスポート
// utils.js
function greet(name) {
  return `こんにちは、${name}さん!`;
}

function farewell(name) {
  return `さようなら、${name}さん!`;
}

const DEFAULT_NAME = 'ゲスト';

// 方法1: オブジェクトとしてまとめてエクスポート
module.exports = {
  greet,
  farewell,
  DEFAULT_NAME,
};

// 方法2: 個別エクスポート
// exports.greet = greet;
// exports.farewell = farewell;
// exports.DEFAULT_NAME = DEFAULT_NAME;
javascript// ESM のエクスポート
// utils.js
export function greet(name) {
  return `こんにちは、${name}さん!`;
}

export function farewell(name) {
  return `さようなら、${name}さん!`;
}

export const DEFAULT_NAME = 'ゲスト';

// デフォルトエクスポート
export default function welcomeMessage(
  name = DEFAULT_NAME
) {
  return `ようこそ、${name}さん!`;
}

インポート構文の比較

javascript// CommonJS のインポート
const utils = require('./utils'); // 全体インポート
const { greet, farewell } = require('./utils'); // 分割代入
const customName = require('./utils').DEFAULT_NAME; // 個別取得

// 動的インポート
if (condition) {
  const dynamicUtils = require('./dynamicUtils');
}
javascript// ESM のインポート
import utils from './utils.js'; // デフォルトインポート
import { greet, farewell } from './utils.js'; // 名前付きインポート
import { greet as sayHello } from './utils.js'; // エイリアス
import * as allUtils from './utils.js'; // 全体インポート

// 動的インポート(Promiseベース)
if (condition) {
  const { dynamicFunction } = await import(
    './dynamicUtils.js'
  );
}

高度なインポート・エクスポートパターン

javascript// ESM の高度なパターン例
// re-export(再エクスポート)
export { greet, farewell } from './utils.js';
export { default as welcome } from './welcome.js';

// 条件付きエクスポート
export const config =
  process.env.NODE_ENV === 'production'
    ? await import('./config.prod.js')
    : await import('./config.dev.js');

// 複数ファイルからの統合エクスポート
export * from './userUtils.js';
export * from './dateUtils.js';
export * from './validationUtils.js';

読み込みタイミングと実行方式の差

CommonJS と ESM では、モジュールの読み込みと実行のタイミングが大きく異なります。

CommonJS の同期的読み込み

javascriptconsole.log('メインスクリプト開始');

// require は同期的に実行される
console.log('モジュール読み込み前');
const utils = require('./utils');
console.log('モジュール読み込み後');

// utils.js の内容がここで完全に利用可能
console.log(utils.greet('田中'));

console.log('メインスクリプト終了');
javascript// utils.js の実行順序
console.log('utils.js の実行開始');

function greet(name) {
  console.log(`greet関数が ${name} で呼ばれました`);
  return `こんにちは、${name}さん!`;
}

console.log('utils.js の実行完了');

module.exports = { greet };

ESM の非同期的読み込み

javascriptconsole.log('メインスクリプト開始');

// import は静的にホイスティングされる
import { greet } from './utils.js';

console.log('インポート文の後');
console.log(greet('田中'));
console.log('メインスクリプト終了');

実行タイミングの詳細比較

#項目CommonJSESM
1読み込みタイミング実行時(同期)パース時(静的解析)
2ホイスティングなしインポート文が先頭に移動
3条件付き読み込み簡単(if 文内で require 可能)動的 import が必要
4キャッシュモジュールオブジェクトバインディング
5循環参照部分的に可能より安全に処理

循環参照の処理

javascript// CommonJS での循環参照(問題が発生しやすい)
// a.js
console.log('a.js 開始');
const b = require('./b');
console.log('a.js での b:', b);

function funcA() {
  return 'Function A';
}

module.exports = { funcA };
console.log('a.js 終了');

// b.js
console.log('b.js 開始');
const a = require('./a'); // この時点で a は空のオブジェクト
console.log('b.js での a:', a); // {}

function funcB() {
  return 'Function B';
}

module.exports = { funcB };
console.log('b.js 終了');
javascript// ESM での循環参照(より安全)
// a.js
console.log('a.js 開始');
import { funcB } from './b.js';
console.log('a.js での funcB:', funcB);

export function funcA() {
  return 'Function A';
}
console.log('a.js 終了');

// b.js
console.log('b.js 開始');
import { funcA } from './a.js'; // 安全に参照可能
console.log('b.js での funcA:', funcA);

export function funcB() {
  return 'Function B';
}
console.log('b.js 終了');

実際のコード例と使い分け

理論的な違いを理解した上で、実際のプロジェクトでどのように使い分けるかを見てみましょう。

CommonJS での実装パターン

Express.js ベースの REST API

javascript// server.js - メインサーバーファイル
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const authMiddleware = require('./middleware/auth');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');

const app = express();
const PORT = process.env.PORT || 3000;

// ミドルウェアの設定
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// レート制限
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  max: 100, // 最大100リクエスト
});
app.use(limiter);

// ルーティング
app.use('/api/users', authMiddleware, userRoutes);
app.use('/api/products', productRoutes);

// エラーハンドリング
app.use(errorHandler);

app.listen(PORT, () => {
  logger.info(`サーバーがポート ${PORT} で起動しました`);
});

module.exports = app;
javascript// utils/database.js - データベース接続管理
const mongoose = require('mongoose');
const logger = require('./logger');

class DatabaseManager {
  constructor() {
    this.connection = null;
  }

  async connect() {
    try {
      const connectionString =
        process.env.MONGODB_URI ||
        'mongodb://localhost:27017/myapp';

      this.connection = await mongoose.connect(
        connectionString,
        {
          useNewUrlParser: true,
          useUnifiedTopology: true,
          maxPoolSize: 10,
          serverSelectionTimeoutMS: 5000,
        }
      );

      logger.info('データベース接続が確立されました');
      return this.connection;
    } catch (error) {
      logger.error('データベース接続エラー:', error);
      throw error;
    }
  }

  async disconnect() {
    if (this.connection) {
      await mongoose.disconnect();
      logger.info('データベース接続を切断しました');
    }
  }

  getConnection() {
    return this.connection;
  }
}

// シングルトンパターンで実装
const dbManager = new DatabaseManager();

module.exports = dbManager;

設定管理システム

javascript// config/index.js - 環境別設定管理
const path = require('path');

// 環境変数の読み込み
require('dotenv').config({
  path: path.join(
    __dirname,
    `../.env.${process.env.NODE_ENV || 'development'}`
  ),
});

const config = {
  app: {
    name: process.env.APP_NAME || 'MyApp',
    port: parseInt(process.env.PORT) || 3000,
    env: process.env.NODE_ENV || 'development',
  },
  database: {
    uri:
      process.env.MONGODB_URI ||
      'mongodb://localhost:27017/myapp',
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      maxPoolSize: 10,
    },
  },
  auth: {
    jwtSecret: process.env.JWT_SECRET || 'fallback-secret',
    jwtExpiry: process.env.JWT_EXPIRY || '24h',
    bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 10,
  },
  email: {
    service: process.env.EMAIL_SERVICE || 'gmail',
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
};

// 環境別の設定をマージ
if (config.app.env === 'production') {
  const productionConfig = require('./production');
  Object.assign(config, productionConfig);
} else if (config.app.env === 'test') {
  const testConfig = require('./test');
  Object.assign(config, testConfig);
}

module.exports = config;

ESM での実装パターン

モダンな Web アプリケーション

javascript// src/app.js - ESM ベースのメインアプリケーション
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';

import userRoutes from './routes/users.js';
import productRoutes from './routes/products.js';
import authMiddleware from './middleware/auth.js';
import errorHandler from './middleware/errorHandler.js';
import logger from './utils/logger.js';
import { DatabaseManager } from './utils/database.js';

const app = express();
const PORT = process.env.PORT || 3000;

// データベース接続
const dbManager = new DatabaseManager();
await dbManager.connect();

// ミドルウェアの設定
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// レート制限
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
});
app.use(limiter);

// ルーティング
app.use('/api/users', authMiddleware, userRoutes);
app.use('/api/products', productRoutes);

// エラーハンドリング
app.use(errorHandler);

app.listen(PORT, () => {
  logger.info(`サーバーがポート ${PORT} で起動しました`);
});

export default app;

モジュラーサービス設計

javascript// services/UserService.js - ユーザー関連ビジネスロジック
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { User } from '../models/User.js';
import {
  ValidationError,
  AuthenticationError,
} from '../utils/errors.js';
import logger from '../utils/logger.js';

export class UserService {
  constructor(config) {
    this.jwtSecret = config.auth.jwtSecret;
    this.jwtExpiry = config.auth.jwtExpiry;
    this.bcryptRounds = config.auth.bcryptRounds;
  }

  async createUser(userData) {
    try {
      const { email, password, name } = userData;

      // 既存ユーザーチェック
      const existingUser = await User.findOne({ email });
      if (existingUser) {
        throw new ValidationError(
          'このメールアドレスは既に使用されています'
        );
      }

      // パスワードハッシュ化
      const hashedPassword = await bcrypt.hash(
        password,
        this.bcryptRounds
      );

      // ユーザー作成
      const user = new User({
        email,
        password: hashedPassword,
        name,
        createdAt: new Date(),
      });

      await user.save();
      logger.info(`新規ユーザー作成: ${email}`);

      // パスワードを除いて返却
      const { password: _, ...userResponse } =
        user.toObject();
      return userResponse;
    } catch (error) {
      logger.error('ユーザー作成エラー:', error);
      throw error;
    }
  }

  async authenticateUser(email, password) {
    try {
      const user = await User.findOne({ email });
      if (!user) {
        throw new AuthenticationError('認証に失敗しました');
      }

      const isValidPassword = await bcrypt.compare(
        password,
        user.password
      );
      if (!isValidPassword) {
        throw new AuthenticationError('認証に失敗しました');
      }

      // JWT トークン生成
      const token = jwt.sign(
        { userId: user._id, email: user.email },
        this.jwtSecret,
        { expiresIn: this.jwtExpiry }
      );

      logger.info(`ユーザーログイン: ${email}`);
      return {
        token,
        user: {
          id: user._id,
          email: user.email,
          name: user.name,
        },
      };
    } catch (error) {
      logger.error('認証エラー:', error);
      throw error;
    }
  }

  async getUserProfile(userId) {
    try {
      const user = await User.findById(userId).select(
        '-password'
      );
      if (!user) {
        throw new ValidationError(
          'ユーザーが見つかりません'
        );
      }
      return user;
    } catch (error) {
      logger.error(
        'ユーザープロフィール取得エラー:',
        error
      );
      throw error;
    }
  }
}

// デフォルトエクスポート
export default UserService;

設定とファクトリーパターン

javascript// config/ConfigManager.js - ESM での設定管理
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

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

export class ConfigManager {
  constructor() {
    this.config = null;
  }

  async loadConfig(environment = 'development') {
    try {
      // 基本設定の読み込み
      const baseConfigPath = join(__dirname, 'base.json');
      const baseConfig = JSON.parse(
        await readFile(baseConfigPath, 'utf8')
      );

      // 環境別設定の読み込み
      const envConfigPath = join(
        __dirname,
        `${environment}.json`
      );
      let envConfig = {};

      try {
        envConfig = JSON.parse(
          await readFile(envConfigPath, 'utf8')
        );
      } catch (error) {
        console.warn(
          `環境設定ファイルが見つかりません: ${envConfigPath}`
        );
      }

      // 設定のマージ
      this.config = {
        ...baseConfig,
        ...envConfig,
        app: {
          ...baseConfig.app,
          ...envConfig.app,
          environment,
          startTime: new Date().toISOString(),
        },
      };

      return this.config;
    } catch (error) {
      console.error('設定読み込みエラー:', error);
      throw error;
    }
  }

  getConfig() {
    if (!this.config) {
      throw new Error(
        '設定が読み込まれていません。loadConfig()を先に実行してください。'
      );
    }
    return this.config;
  }

  get(path) {
    const keys = path.split('.');
    let value = this.getConfig();

    for (const key of keys) {
      value = value?.[key];
      if (value === undefined) break;
    }

    return value;
  }
}

// シングルトンインスタンス
export const configManager = new ConfigManager();

// 便利な関数もエクスポート
export async function initializeConfig(environment) {
  return await configManager.loadConfig(environment);
}

export function getConfig(path) {
  return configManager.get(path);
}

相互運用の方法

現実のプロジェクトでは、CommonJS と ESM の混在環境が発生することがよくあります。

ESM から CommonJS モジュールを使用

javascript// ESM プロジェクトで CommonJS パッケージを使用
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// CommonJS パッケージの読み込み
const oldLibrary = require('old-commonjs-library');

// 動的インポートでの CommonJS 読み込み
const { default: express } = await import('express');
const app = express();

// 条件付きの動的インポート
let processor;
if (process.env.USE_NEW_PROCESSOR === 'true') {
  const { NewProcessor } = await import(
    './processors/new.js'
  );
  processor = new NewProcessor();
} else {
  const OldProcessor = require('./processors/old');
  processor = new OldProcessor();
}

CommonJS プロジェクトで ESM を使用

javascript// CommonJS プロジェクトで ESM モジュールを使用
async function loadESMModule() {
  try {
    // 動的インポートを使用
    const { someFunction } = await import(
      './esm-module.js'
    );
    return someFunction;
  } catch (error) {
    console.error('ESM モジュール読み込みエラー:', error);
    throw error;
  }
}

// 使用例
async function main() {
  const esmFunction = await loadESMModule();
  const result = esmFunction('test data');
  console.log(result);
}

main().catch(console.error);

package.json での設定管理

json{
  "name": "hybrid-project",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./src/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "import": "./src/utils/index.js",
      "require": "./dist/utils.cjs"
    }
  },
  "scripts": {
    "build": "yarn build:esm && yarn build:cjs",
    "build:esm": "tsc",
    "build:cjs": "tsc --module commonjs --outDir dist-cjs"
  }
}

まとめ

Node.js におけるモジュールシステムの選択は、プロジェクトの将来性と開発効率に大きく影響する重要な決定です。CommonJS と ESM それぞれに明確な特徴と適用場面があることを理解していただけたでしょうか。

この記事で解説した内容をまとめると、以下のポイントが重要です:

モジュールシステムの本質的価値

  • コードの分割と再利用による保守性の向上
  • 明示的な依存関係管理による品質向上
  • スコープ分離による安全性の確保

CommonJS の特徴と適用場面

  • 動的な読み込みが可能で柔軟性が高い
  • 既存の Node.js エコシステムとの互換性が優秀
  • サーバーサイド特化のプロジェクトに最適

ESM の特徴と将来性

  • 言語標準として長期的にサポート
  • ブラウザとの互換性によるコード共有が可能
  • 静的解析による最適化とツールサポートが優秀

実践的な選択指針

  • 新規プロジェクト:ESM を推奨
  • 既存 CommonJS プロジェクト:段階的な移行を検討
  • ライブラリ開発:両方式に対応することを推奨
  • チーム開発:統一されたモジュールシステムの採用

現在の Web 開発トレンドを考慮すると、ESM が将来の主流となることは間違いありません。しかし、CommonJS も当面は重要な役割を果たし続けるでしょう。重要なのは、それぞれの特徴を理解し、プロジェクトの要件に応じて適切な選択をすることです。

モジュールシステムの理解を深めることで、より構造化された、保守しやすい Node.js アプリケーションの開発が可能になります。この知識を活用して、効率的で未来指向の開発を進めていきましょう。

関連リンク