Answer: historical reasons.
You're right, we may only need module and exports , but they still exist for backward compatibility.
There used to be a time when the module shell was changed in almost all fix releases.
In Node 0.1.11, the module wrapper was:
var wrapper = "function (__filename) { "+ " var onLoad; "+ " var onExit; "+ " var exports = this; "+ content+ "\n"+ " this.__onLoad = onLoad;\n"+ " this.__onExit = onExit;\n"+ "};\n";
See: https://github.com/nodejs/node/blob/v0.1.11/src/node.js#L167#L177
As you can see, exports was the same as this , with which the wrapper function was called. You could not exchange it with a new object, and you could not even add some reserved keys to it - for example. you cannot safely export a property named __onExit .
Then at 0.1.12 it was:
var wrapper = "function (__filename, exports) { " + content + "\n};";
See: https://github.com/nodejs/node/blob/v0.1.12/src/node.js#L243-L245
Here, exports was an object provided as one of the arguments, but you could not exchange it for a new object, you could add or remove only the properties of the object that you had.
Then 0.1.13 was the first to have this, i.e. require and include :
var wrapper = "function (__filename, exports, require, include) { " + content + "\n};";
See: https://github.com/nodejs/node/blob/v0.1.13/src/node.js#L225-L227
Then 0.1.14 was the first one to have __module (with underscores) in the wrapper (and this dropped include ):
var wrapper = "var __wrap__ = function (__module, __filename, exports, require) { " + content + "\n}; __wrap__;";
See: https://github.com/nodejs/node/blob/v0.1.14/src/node.js#L280-L284
And 0.1.16 was the first to have a module argument in the wrapper (without underscores):
var wrapper = "var __wrap__ = function (exports, require, module, __filename) { " + content + "\n}; __wrap__;";
See: https://github.com/nodejs/node/blob/v0.1.16/src/node.js#L444-L448
After that, it was changed many times, but this is the time when module introduced, as a result of which exports no longer needed, but still a useful shortcut that allows you to use:
exports.a = 1; exports.b = 2; exports.c = 3;
instead:
module.exports.a = 1; module.exports.b = 2; module.exports.c = 3;
although in practice, if there were no exports , then they usually write:
const exports = module.exports; exports.a = 1; exports.b = 2; exports.c = 3;
or more likely:
module.exports = { a: 1, b: 2, c: 3, };
or, to have some checks in static analysis tools:
const a = 1; const b = 2; const c = 3; module.exports = { a, b, c };
There are many ways to do this; this is a fairly flexible mechanism.