What is the difference between CommonJS and ES modules in Node.js?
CommonJS (
require) is Node's original synchronous system; ES modules (import) are the standard, linked before the code runs.CommonJS ES modules Top-level awaitno yes __dirnameyes no, import.meta.dirnameExtension in relative paths optional required Imported values read once at requirelive bindings Since Node 22.12 (and 20.19), CommonJS can
require()an ES module without top-levelawait. See CommonJS vs ES modules.What happens when you reassign
exportsinstead ofmodule.exports?JavaScriptexports.a = 1; exports = { b: 2 }; module.exports.c = 3; console.log(JSON.stringify(module.exports));exportsstarts as a reference tomodule.exports, which is whatrequirereturns.exports.a = 1adds to that object;exports = { b: 2 }only rebinds the local variable. To export one function or class, assignmodule.exports.How does
requirework, and where doexports,requireand__dirnamecome from?Node wraps each CommonJS file in a function called with
exports,require,module,__filenameand__dirname, so those names are local to each file.JavaScript// a CommonJS file: these names come from the module wrapper console.log(arguments.length); console.log(typeof exports, typeof require, typeof module); console.log(typeof __filename, typeof __dirname); console.log(require.main === module);requireresolves the path, runs the file once, cachesmodule.exportsand returns it.require.main === modulemeans the file was run directly.Are Node.js modules cached? What does that mean for singletons?
Yes. The first
requireruns the file and caches it by resolved path; later calls return the samemodule.exports. So an exported database pool is a singleton per process.// counter.js let count = 0; module.exports = { next: () => ++count }; // app.js const a = require('./counter'); const b = require('./counter'); console.log(a === b, a.next(), b.next()); // true 1 2Traps: one package installed in two
node_modulesfolders gives two instances, and eachclusterworker or worker thread has its own cache.How does Node.js resolve the path in
require('something')?In this order:
- Core modules:
fsornode:fs(node:testexists only with the prefix). - A path starting with
./,../or/: the exact name, then.js,.json,.node, then a folder'smainorindex.js. - A package:
node_modulesin this folder, then in each parent up to the root, then the package'sexportsfield (ormain).
ES modules do not try extensions, and
exportsblocks paths it does not list.- Core modules:
What happens with circular dependencies in Node.js?
Node hands out a partly filled module. When
a.jsandb.jsrequire each other,bgetsa'smodule.exportsas it is at that moment.// a.js exports.loaded = false; const b = require('./b'); console.log('in a, b.loaded =', b.loaded); exports.loaded = true; // b.js const a = require('./a'); console.log('in b, a.loaded =', a.loaded); exports.loaded = true; // node a.js prints: // in b, a.loaded = false // in a, b.loaded = trueIn ES modules, reading an imported
let,constorclasstoo early throws aReferenceError. Fix the design: move the shared code into a third module, orrequirelazily inside a function.What is dynamic
import()and when do you use it in Node.js?import()loads a module at runtime and returns a promise for it, in CommonJS and ES modules, with any expression as the specifier.JavaScriptasync function load(name) { // the module is chosen at runtime, which a static import cannot do return import(name); } load('node:path').then((path) => { console.log(path.join('reports', '2026', 'march.csv')); console.log(path.extname('archive.tar.gz')); });Use it to load an ES-only package from CommonJS, plugins named in config, or a heavy module only when needed.
How does Node.js decide whether a file is CommonJS or an ES module?
.mjsis always an ES module,.cjsalways CommonJS. A.jsfile follows the nearestpackage.json"type":"module"means ESM,"commonjs"CommonJS. With notypefield (or nopackage.json), Node 22.7 and later try CommonJS first and rerun a file that usesimportorexportas ESM (syntax detection); that costs a second parse and warns when apackage.jsonexists, so set"type"explicitly. See ES modules.What are core modules in Node.js, and what does the
node:prefix mean?Core modules ship with Node.js itself:
fs,path,http,crypto,events,stream,osand more, loaded with nonpm install. Thenode:prefix says plainly that you mean the built-in, never a package with the same name.JavaScriptconst path = require('node:path'); const util = require('node:util'); const { createHash } = require('node:crypto'); console.log(path.basename('/var/log/app/error.log', '.log')); console.log(util.format('%s has %d items', 'cart', 3)); console.log(createHash('sha256').update('coddy').digest('hex').slice(0, 16)); console.log(require('fs') === require('node:fs'));Both names return the same cached module. Newer built-ins such as
node:testandnode:sqliteexist only with the prefix.What does the
exportsfield in package.json do?exportslists a package's public entry points. It takes priority overmain, can serve ES modules and CommonJS from one package through conditions, and blocks every path it does not list.{ "name": "my-lib", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, "./utils": "./dist/utils.mjs" } }Now
require('my-lib/dist/internal.js')fails withERR_PACKAGE_PATH_NOT_EXPORTED. Conditions are checked in order, so putdefaultlast. Addingexportsto a published package is a breaking change.
Node.js Modules Interview Questions
How require and import find, load and cache code, and where the two systems differ.
10 questions