Node.jsには、ファイルを分割して読み込む「モジュール」の方式が2種類あります。古くからあるrequireを使うCommonJSと、新しいimportを使うESモジュール(ES Modules)です。どちらも「他のファイルの関数や値を読み込む」目的は同じですが、書き方や挙動が違います。
この2つが混在すると、「importが使えない」「__dirnameがエラーになる」「拡張子が必要と言われる」といったエラーに悩まされます。原因のほとんどは、今どちらの方式で動いているかを把握していないことです。この記事では、実機のNode.jsで確認しながら、両者の違いと使い分けを整理します。
- CommonJSは
const 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を使います。
// 公開する側(module.exports)
function add(a, b) {
return a + b;
}
module.exports = { add };
// 読み込む側(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で複数を公開できます。
// 名前付き export と default export
export function add(a, b) {
return a + b;
}
export default function greet() {
return "hi";
}
// 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モジュールになります。
{
"name": "my-app",
"type": "module"
}
実機でも、package.jsonに"type": "module"を書くと、.jsファイルがESモジュールとして扱われることを確認しました。逆に、"type": "module"の環境でCommonJSを書きたいファイルは、拡張子を.cjsにします。「今このファイルはどちらか」は、拡張子とpackage.jsonで決まると覚えておくと、エラーの原因を切り分けやすくなります。
【最重要】ESモジュールでは__dirnameが使えない
CommonJSから移行したときに、最も多くの人がつまずくのがこれです。ESモジュールには、__dirname(ファイルのあるフォルダ)と__filenameが存在しません。使おうとするとReferenceErrorになります。
// 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); // このファイルがあるフォルダ
実機で確認したところ、ESモジュールで__dirnameを使うとReferenceErrorになり、typeof __dirnameはundefinedでした。ESモジュールでファイルの場所を基準にパスを組み立てたいときは、import.meta.urlをfileURLToPathで変換し、path.dirname()でフォルダ部分を取り出します。この4行はESモジュールの定番パターンなので、覚えておくと便利です。
importでは拡張子が必須
もう一つの定番のつまずきが、拡張子です。CommonJSのrequire("./math")は拡張子を省略できますが、ESモジュールのimportでは.jsまで書く必要があります。
// 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モジュールでは追加の指定が必要です。
// CommonJS: require で JSON をそのまま読める
const data = require("./data.json");
console.log(data.name);
// 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の読み込みは比較的簡単ですが、逆は少し手間がかかります。
// 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.urlをfileURLToPathで変換して作ります。
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を使います。
よくある質問
import)がおすすめです。既存のCommonJSプロジェクトは、無理に書き換えずそのままrequireでも問題ありません。大切なのは、1つのプロジェクト内で方式を統一することです。.mjsにするか、package.jsonに"type": "module"を追加します。__dirnameはありません。import { fileURLToPath } from "node:url"とimport { dirname } from "node:path"を使い、dirname(fileURLToPath(import.meta.url))で同じものを作ります。.jsまで書く必要があります。CommonJSのrequireは省略できます。TypeScriptやバンドラーを使う場合は設定によって異なります。importでCommonJSを読むのは簡単です。逆にCommonJSからESモジュールを読むにはrequireが使えないため、await import()の動的importを使います。基本は1つの方式に統一するのがおすすめです。まとめ
- CommonJSは
require/module.exports、ESモジュールはimport/exportです。 - ESモジュールは
.mjsかpackage.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モジュール、既存はそのまま、という方針で、方式を統一して使うのがおすすめです。
