Node.jsでの開発を進める中で、多くのエンジニアが最初に直面する壁の一つがモジュールのエクスポート方法です。
特に「exports」と「module.exports」は非常によく似ており、どちらを使うべきか迷う場面も少なくありません。
これら二つの違いを正しく理解していないと、意図した通りにモジュールが読み込めず、実行時エラーに悩まされる原因となります。
本記事では、2026年現在のNode.js環境におけるこれら二つの違いと、適切な使い分けについて詳しく解説します。
JavaScriptの参照の仕組みに基づいた動作原理を紐解くことで、現場で役立つ実践的な知識を身につけていきましょう。
Node.jsにおけるモジュールシステムの基礎知識
Node.jsでは、プログラムを機能ごとに分割して管理するために「CommonJS」と呼ばれるモジュールシステムが長らく採用されてきました。
現在ではECMAScript Modules (ESM) も広く普及していますが、既存のライブラリや特定のサーバーサイド環境では依然としてCommonJSの仕組みが重要です。
モジュールシステムを利用することで、あるファイルで定義した変数や関数を、別のファイルから require() 関数を使って呼び出すことが可能になります。
この際、外部に「何を公開するか」を定義するために使用されるのが、今回注目する exports と module.exports です。
Node.jsは各ファイルを独立したスコープ(モジュール)として扱い、その中には module という特別なオブジェクトが自動的に用意されています。
exportsとmodule.exportsの決定的な違い
結論から述べると、exportsはmodule.exportsへのショートカット(参照)に過ぎません。
Node.jsの内部では、便宜上以下のような代入が事前に行われているとイメージすると分かりやすいでしょう。
// Node.js内部でのイメージ
let exports = module.exports;
初期状態では、module.exports は空のオブジェクト {} を指しており、exports も同じオブジェクトを指しています。
そのため、exports にプロパティを追加することは、間接的に module.exports にプロパティを追加することと同義になります。
しかし、JavaScriptにおけるオブジェクトの参照ルールにより、この関係性が崩れる瞬間があります。
この「参照の断絶」こそが、開発者が最も注意すべきポイントであり、トラブルの元となる部分です。
module.exportsの役割と挙動
module.exports は、そのファイルが require() されたときに、実際に呼び出し元に返されるオブジェクトそのものです。
Node.jsはこの module.exports の中身だけを信頼し、エクスポートの成果物として扱います。
そのため、module.exports に対して直接値を代入したり、関数を割り当てたりすると、その値がそのままエクスポートされます。
exportsの役割と注意点
exports は、複数のプロパティをエクスポートする際に記述を短くするための補助的な変数です。
exports.name = 'value'; のように記述すれば、module.exports に name プロパティが追加されます。
ただし、exports = ... のように、exports自体に新しい値を直接代入してはいけません。
直接代入を行ってしまうと、exports 変数が持っていた module.exports への参照が上書きされ、全く別のオブジェクトを指すようになってしまうからです。
具体的なコード例で見る使い方の違い
実際にどのように動作が異なるのか、具体的なソースコードとその実行結果を確認してみましょう。
複数の機能をエクスポートする場合
関数や定数を複数公開したい場合は、exports を使うのが一般的で簡潔です。
// math.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
この場合、呼び出し側では以下のように利用できます。
// main.js
const math = require('./math.js');
console.log(math.add(5, 3));
console.log(math.subtract(10, 4));
8
6
ここでは、exports を通じて module.exports という一つのオブジェクトに add と subtract が追加されています。
単一の関数やクラスをエクスポートする場合
モジュール全体を一つの関数やクラスとして公開したい場合は、必ず module.exports を使用します。
// logger.js
module.exports = (message) => {
console.log(`Log: ${message}`);
};
// main.js
const log = require('./logger.js');
log('Hello Node.js 2026');
Log: Hello Node.js 2026
もしこれを exports = ... と書いてしまうと、呼び出し元では空のオブジェクトが返ってきてしまい、エラーが発生します。
なぜexportsへの直接代入は失敗するのか
この挙動をより深く理解するために、JavaScriptの変数参照の仕組みを整理しましょう。
Node.jsがモジュールを実行する際、暗黙的に以下のようなラッパー関数でコードを包んでいます。
(function(exports, require, module, __filename, __dirname) {
// あなたが書いたコードがここに配置される
});
関数の引数として exports と module が渡されており、この時点で exports === module.exports は true です。
しかし、関数内で exports = function() {} と記述すると、引数としての exports というローカル変数の向き先が変わるだけになります。
大元の module オブジェクトの中にある exports プロパティは、初期状態の空オブジェクト {} のまま変化しません。
その結果、require() を実行した側には、期待した関数ではなく空のオブジェクトが渡されることになります。
2026年における推奨される使い分けルール
現代のNode.js開発においては、コードの可読性とメンテナンス性を高めるために以下の指針を持つことが推奨されます。
| 使用ケース | 推奨される手法 | 理由 |
|---|---|---|
| 一つのクラスや関数だけを返したい | module.exports = ... | モジュールを単一の責任に特化させるため |
| 複数の便利な関数をまとめたい | exports.name = ... | タイピング量を減らし、コードをスッキリさせるため |
| 混乱を避けたい | 常に module.exports を使う | 一貫性を保ち、参照切れのミスを防ぐため |
チーム開発においては、「迷ったら module.exports を使う」というルールを設けるだけでも、不必要なバグを防ぐことができます。
また、最新のプロジェクトであれば、CommonJSではなくESM(export default や export const)への移行を検討することも重要です。
よくあるエラーとトラブルシューティング
モジュールのエクスポートに関連して頻出するエラーの例を確認しておきましょう。
TypeError: object is not a function
このエラーは、関数をエクスポートしたつもりが、実際にはオブジェクト(あるいは空のオブジェクト)が返されている場合に発生します。
原因の多くは、前述した「exports への直接代入」です。
// 誤った例
exports = function() { return "error"; };
この場合、呼び出し側で const myFunc = require('./file') としても、myFunc は {} になり、実行時にエラーとなります。
混在による混乱
一つのファイル内で exports.someProperty = ... と module.exports = ... を混在させると非常に危険です。
もし module.exports = { ... } と記述してオブジェクトを再代入した後に exports.other = ... と書いても、後者は無視されます。
なぜなら、module.exports が新しいオブジェクトを指した時点で、古いオブジェクトを指し続けている exports との繋がりが完全に切れてしまうからです。
ECMAScript Modules (ESM) との比較
2026年現在、Node.jsでは package.json に "type": "module" を記述することで、標準的な import/export 構文が利用可能です。
ESMでは、今回のような exports と module.exports の混乱は起こりません。
// ESMの例
export const add = (a, b) => a + b;
export default class Calculator {}
CommonJSとESMは相互運用が可能ですが、その挙動には細かい制約があります。
ESMからCommonJSを読み込む場合、module.exports の内容が default エクスポートとして扱われることが多いことを覚えておきましょう。
最新のランタイム環境(Node.js 24以降など)では、これらの境界線もよりスムーズに扱えるよう改善が進んでいますが、根本的な仕組みの理解は不可欠です。
まとめ
Node.jsにおける exports と module.exports の違いについて解説してきました。
exports はあくまで module.exports への便利な参照であり、最終的に評価されるのは常に module.exports であるという点が最も重要です。
プロパティを追加するだけなら exports が便利ですが、モジュールそのものを関数やクラスとして定義したい場合は module.exports を使用してください。
もし仕組みが複雑だと感じる場合は、常に module.exports に統一して記述することで、予期せぬ参照エラーを防ぐことができます。
適切なエクスポート手法を選択することは、堅牢でメンテナンス性の高いアプリケーションを構築するための第一歩です。
今回学んだ知識を活かして、よりクリーンなモジュール設計を目指していきましょう。
