【Node.js】requireとimportの違い|CommonJSとESモジュールの使い分け

【Node.js】requireとimportの違い|CommonJSとESモジュールの使い分け Node.js

Node.jsには、ファイルを分割して読み込む「モジュール」の方式が2種類あります。古くからあるrequireを使うCommonJSと、新しいimportを使うESモジュール(ES Modules)です。どちらも「他のファイルの関数や値を読み込む」目的は同じですが、書き方や挙動が違います。

この2つが混在すると、「importが使えない」「__dirnameがエラーになる」「拡張子が必要と言われる」といったエラーに悩まされます。原因のほとんどは、今どちらの方式で動いているかを把握していないことです。この記事では、実機のNode.jsで確認しながら、両者の違いと使い分けを整理します。

先に結論

  • CommonJSconst x = require("./file")module.exports。Node.jsの従来の標準です。
  • ESモジュールimport x from "./file.js"export。ブラウザと共通の新しい標準です。
  • ESモジュールを有効にするには、拡張子を.mjsにするか、package.json"type": "module"を書きます。
  • ESモジュールでは__dirnameが使えませんimport.meta.urlから作ります。
  • ESモジュールのimportでは、ファイルの拡張子(.js)が必須です。
  • 新規プロジェクトはESモジュール推奨ですが、既存資産が多い場合はCommonJSのままでも問題ありません。

Node.jsのセットアップは指定したバージョンをインストールする方法NVMでバージョン切り替え、JSONの扱いはJSONファイルの読み込み方もあわせて参考になります。

スポンサーリンク

2つのモジュール方式(CommonJSとESモジュール)

まず全体像です。同じ「読み込み」でも、キーワードと挙動が次のように違います。

項目 CommonJS ESモジュール
読み込み require("./file") import x from "./file.js"
公開 module.exports = ... export ...
有効化 既定(.js .mjs"type": "module"
読み込みの種類 同期・どこでも書ける 静的・先頭に集約(動的はimport()
__dirname 使える 使えない(import.meta.url
import時の拡張子 省略できる 必須(.jsまで書く)

CommonJS:require と module.exports

CommonJSは、Node.jsで長く標準だった方式です。.jsファイルは、特別な設定をしなければCommonJSとして動きます。公開する側はmodule.exports、読み込む側はrequireを使います。

math.cjs
// 公開する側(module.exports)
function add(a, b) {
    return a + b;
}

module.exports = { add };
main.cjs
// 読み込む側(require)
const { add } = require("./math.cjs");

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

// __dirname(このファイルがあるフォルダ)が使える
console.log(typeof __dirname);   // string

実機でも、require("./math.cjs")addを読み込んで5が得られ、__dirnameが文字列として使えることを確認しました。requireは同期的に読み込むため、if文の中など、コードのどこにでも書けるのが特徴です。

ESモジュール:import と export

ESモジュールは、ブラウザのJavaScriptと共通の新しい標準です。公開する側はexport、読み込む側はimportを使います。export defaultで「既定の1つ」を、名前付きexportで複数を公開できます。

math.mjs
// 名前付き export と default export
export function add(a, b) {
    return a + b;
}

export default function greet() {
    return "hi";
}
main.mjs
// default は名前なし、名前付きは { } で受け取る
import greet, { add } from "./math.mjs";

console.log(add(2, 3));   // 5
console.log(greet());     // hi

実機でも、import greet, { add } from "./math.mjs"でdefaultと名前付きの両方を読み込めました。importはファイルの先頭にまとめて書くのが基本で、読み込みはファイルの実行前に解決されます(静的)。

ESモジュールを有効にする(.mjs / type:module)

.jsファイルは既定でCommonJSです。ESモジュールとして動かすには、次のどちらかが必要です。

  • 拡張子を.mjsにする:そのファイルだけESモジュールになります。
  • package.json"type": "module"を書く:そのプロジェクトの.jsがすべてESモジュールになります。
package.json
{
  "name": "my-app",
  "type": "module"
}

実機でも、package.json"type": "module"を書くと、.jsファイルがESモジュールとして扱われることを確認しました。逆に、"type": "module"の環境でCommonJSを書きたいファイルは、拡張子を.cjsにします。「今このファイルはどちらか」は、拡張子とpackage.jsonで決まると覚えておくと、エラーの原因を切り分けやすくなります。

【最重要】ESモジュールでは__dirnameが使えない

CommonJSから移行したときに、最も多くの人がつまずくのがこれです。ESモジュールには、__dirname(ファイルのあるフォルダ)と__filename存在しません。使おうとするとReferenceErrorになります。

dirname-esm.mjs
// NG: ESモジュールでは __dirname は未定義(ReferenceError)
// console.log(__dirname);

// OK: import.meta.url から作る
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

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

console.log(__dirname);   // このファイルがあるフォルダ
__dirnameはimport.meta.urlから作る

実機で確認したところ、ESモジュールで__dirnameを使うとReferenceErrorになり、typeof __dirnameundefinedでした。ESモジュールでファイルの場所を基準にパスを組み立てたいときは、import.meta.urlfileURLToPathで変換し、path.dirname()でフォルダ部分を取り出します。この4行はESモジュールの定番パターンなので、覚えておくと便利です。

importでは拡張子が必須

もう一つの定番のつまずきが、拡張子です。CommonJSのrequire("./math")は拡張子を省略できますが、ESモジュールのimportでは.jsまで書く必要があります

extension.mjs
// NG: 拡張子なしはエラー(ERR_MODULE_NOT_FOUND)
// import { add } from "./math";

// OK: 拡張子まで書く
import { add } from "./math.mjs";

console.log(add(1, 1));   // 2

実機でも、拡張子を省いたimport { add } from "./math"ERR_MODULE_NOT_FOUND(モジュールが見つからない)になり、"./math.mjs"と書くと正しく読み込めました。TypeScriptやバンドラーを使う環境では省略できる場合もありますが、素のNode.jsでESモジュールを書くときは拡張子を必ず付けてください。

JSONを読み込む(requireとimportの違い)

設定ファイルなどでJSONを読み込む場面でも、両者で書き方が違います。CommonJSはrequireでそのまま読めますが、ESモジュールでは追加の指定が必要です。

json-cjs.cjs
// CommonJS: require で JSON をそのまま読める
const data = require("./data.json");
console.log(data.name);
json-esm.mjs
// ESモジュール: import に type: json を付ける
import data from "./data.json" with { type: "json" };
console.log(data.name);

// または createRequire を使う(確実な代替)
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const config = require("./config.json");

実機でも、CommonJSのrequire("./data.json")はそのままJSONを読み込め、ESモジュールではwith { type: "json" }を付けるかcreateRequireを使う方法の両方で読み込めました。JSONをファイルとして読みたいだけなら、fsで読んでJSON.parseする方法もあります。

CommonJSとESモジュールを混在させる

既存のCommonJS資産を、ESモジュールから使いたいこともあります。ESモジュールからCommonJSの読み込みは比較的簡単ですが、逆は少し手間がかかります。

interop.mjs
// ESモジュールから CommonJS を import(default として取り込む)
import pkg from "./math.cjs";
console.log(pkg.add(4, 5));   // 9

// CommonJS から ESモジュールを読むには動的 import を使う
// (require では ESモジュールを読めない)
const mod = await import("./math.mjs");
console.log(mod.add(10, 20));   // 30

実機でも、ESモジュールからimport pkg from "./math.cjs"でCommonJSモジュールを読み込めました(module.exportsの中身がdefaultとして入ります)。逆に、CommonJSからESモジュールを読むにはrequireが使えないため、await import("./file.mjs")という動的importを使います。動的importは、条件によって読み込みを変えたいときにも便利です。

どちらを使うべきか(使い分け)

これから新しく作るなら、ブラウザと共通で将来性のあるESモジュールがおすすめです。一方、既存のCommonJSプロジェクトを無理に書き換える必要はありません。

  • 新規プロジェクト:ESモジュール("type": "module")。ブラウザと書き方をそろえられます。
  • 既存のCommonJSプロジェクト:そのままCommonJSでも問題ありません。無理に混在させると複雑になります。
  • 古いライブラリを多用する:CommonJSのほうが相性が良い場合があります。

大切なのは、1つのプロジェクトの中で方式を統一することと、「今このファイルはどちらか」を常に意識することです。

よくある失敗

importを書いたのに「使えない」と言われる

そのファイルがCommonJSとして動いているためです。.mjsにするか、package.json"type": "module"を追加してESモジュールにします。

ESモジュールで__dirnameがReferenceErrorになる

ESモジュールに__dirnameはありません。import.meta.urlfileURLToPathで変換して作ります。

importで拡張子を省略してモジュールが見つからない

ESモジュールのimportは拡張子が必須です。"./math"ではなく"./math.js"と書きます。

type:moduleの環境でCommonJSを書いてエラー

"type": "module"の環境では.jsがESモジュールになります。CommonJSで書きたいファイルは拡張子を.cjsにします。

CommonJSからESモジュールをrequireしようとする

requireではESモジュールを読み込めません。await import("./file.mjs")の動的importを使います。

よくある質問

Qrequireとimportはどちらを使うべきですか?
A新規プロジェクトなら、ブラウザと共通で将来性のあるESモジュール(import)がおすすめです。既存のCommonJSプロジェクトは、無理に書き換えずそのままrequireでも問題ありません。大切なのは、1つのプロジェクト内で方式を統一することです。
Qimportが使えないのはなぜですか?
AそのファイルがCommonJSとして動いているためです。ESモジュールにするには、拡張子を.mjsにするか、package.json"type": "module"を追加します。
QESモジュールで__dirnameを使うには?
AESモジュールに__dirnameはありません。import { fileURLToPath } from "node:url"import { dirname } from "node:path"を使い、dirname(fileURLToPath(import.meta.url))で同じものを作ります。
Qimportで拡張子は省略できますか?
A素のNode.jsのESモジュールでは省略できず、.jsまで書く必要があります。CommonJSのrequireは省略できます。TypeScriptやバンドラーを使う場合は設定によって異なります。
QCommonJSとESモジュールは混在できますか?
AESモジュールからimportでCommonJSを読むのは簡単です。逆にCommonJSからESモジュールを読むにはrequireが使えないため、await import()の動的importを使います。基本は1つの方式に統一するのがおすすめです。

まとめ

  • CommonJSはrequiremodule.exports、ESモジュールはimportexportです。
  • ESモジュールは.mjspackage.json"type": "module"で有効にします。
  • ESモジュールでは__dirnameが使えませんimport.meta.urlから作ります。
  • ESモジュールのimport拡張子が必須です。
  • JSONはrequireでそのまま、ESモジュールではwith { type: "json" }で読みます。
  • ESモジュールからCommonJSはimport、逆は動的import()を使います。

requireとimportの違いは、「今このファイルがCommonJSかESモジュールか」を意識すれば、ほとんどのエラーは解決できます。新規ならESモジュール、既存はそのまま、という方針で、方式を統一して使うのがおすすめです。