Learn Node's two module systems, CommonJS with require and ES Modules with import, how Node picks between them and which one to use for new code.

Bishal Biswas
Self Learner
What is a module?
A module is a JavaScript file that shares some of its code with other files.
Without modules, a growing program becomes one enormous file. With modules, you split it up: one file for maths helpers, one for talking to the database, one for the web server. Each file exports what others may use, and imports what it needs.
Think of a kitchen with labelled drawers. Cutlery goes in one, baking tools in another. When you need a whisk, you open the baking drawer instead of searching the whole kitchen.
Two systems, one Node
Node has two module systems, for historical reasons:
- CommonJS (CJS): Node’s original system from 2009. Uses
require()andmodule.exports. - ES Modules (ESM): the official JavaScript standard, also used by browsers. Uses
importandexport.
Both are fully supported in Node 24. New code should use ES Modules, but you will read plenty of CommonJS in older projects and tutorials, so you need to recognise both.
CommonJS: require and module.exports
A CommonJS file puts what it wants to share on module.exports:
// prices.cjs
function addTax(amount) {
return amount * 1.2;
}
const currency = 'GBP';
module.exports = { addTax, currency };
Another file loads it with require(), which returns whatever module.exports was:
// shop.cjs
const { addTax, currency } = require('./prices.cjs');
const fs = require('node:fs'); // built-in modules work the same way
console.log(addTax(10), currency); // prints: 12 GBP
node shop.cjs
You will also see a shorter form, which adds properties one at a time:
exports.addTax = (amount) => amount * 1.2;
exports.currency = 'GBP';
ES Modules: import and export
The same code as an ES module:
// prices.mjs
export function addTax(amount) {
return amount * 1.2;
}
export const currency = 'GBP';
// shop.mjs
import { addTax, currency } from './prices.mjs';
import fs from 'node:fs';
console.log(addTax(10), currency); // prints: 12 GBP
These are called named exports: each one has a name, and you import it by that name inside { }.
Default exports
A module can also have one default export, its “main thing”. You import it without braces and can give it any name you like:
// greet.mjs
export default function greet(name) {
return `Hello, ${name}!`;
}
// app.mjs
import sayHello from './greet.mjs'; // any name works for a default import
console.log(sayHello('Maya')); // prints: Hello, Maya!
How Node decides which system a file uses
This is the part that trips people up. Node looks at the file extension first:
| File | Treated as |
|---|---|
.mjs |
Always an ES module |
.cjs |
Always CommonJS |
.js |
Depends on the nearest package.json |
For .js files, Node finds the nearest package.json (in the same folder, or the closest parent folder) and reads its "type" field:
{
"name": "shop",
"type": "module"
}
"type": "module":.jsfiles are ES modules."type": "commonjs":.jsfiles are CommonJS.- No
"type"at all: Node treats.jsas CommonJS, unless the file containsimportorexport, in which case it runs it as an ES module. When this happens inside a project that has apackage.json, Node prints a warning asking you to add"type": "module", because guessing costs a little speed.
Tip: For new projects, add
"type": "module"topackage.jsonand use plain.jsfiles everywhere. You can set it from the terminal withnpm pkg set type=module.
The key differences
File extensions are required in ESM
CommonJS lets you leave off the extension: require('./prices') finds prices.js. ES modules want the full file name:
import { addTax } from './prices.js'; // the .js is required
Built-in modules (node:fs) and installed packages (express) do not need an extension.
Top-level await only in ESM
ES modules can use await outside any function. CommonJS cannot:
// config.mjs
import { readFile } from 'node:fs/promises';
const text = await readFile('settings.json', 'utf8'); // fine in ESM
console.log(JSON.parse(text));
__dirname vs import.meta
CommonJS gives every file two handy variables: __dirname (the folder the file is in) and __filename (the file’s full path). ES modules do not have them. Use import.meta instead:
// where.mjs
console.log(import.meta.dirname); // the folder, like __dirname
console.log(import.meta.filename); // the full path, like __filename
console.log(import.meta.url); // the same path as a file:// URL
Globals: process, __dirname and import.meta goes deeper.
Loading JSON
CommonJS can require a JSON file directly. ES modules use an import attribute that says “this is JSON”:
// CommonJS
const settings = require('./settings.json');
// ES module
import settings from './settings.json' with { type: 'json' };
Strict mode
ES modules always run in strict mode, a safer flavour of JavaScript that turns some silent mistakes into errors. For example, assigning to a variable you never declared:
total = 5; // CommonJS: quietly creates a global. ESM: ReferenceError: total is not defined
Summary table
| CommonJS | ES Modules | |
|---|---|---|
| Share code | module.exports = ... |
export / export default |
| Load code | require('./file') |
import ... from './file.js' |
| Extension in path | Optional | Required |
Top-level await |
No | Yes |
| Current folder | __dirname |
import.meta.dirname |
| Strict mode | Only if you opt in | Always |
| Used in browsers | No | Yes |
Which should you use?
Use ES Modules for anything new. It is the JavaScript standard, it works in browsers too, and the whole ecosystem is moving towards it.
Use CommonJS when you are working in an existing CommonJS project. Converting a big project is a job of its own.
The two systems can load each other. The next lesson, require(esm) and Module Interop, shows exactly how.
Common mistakes
“Cannot use import statement outside a module”
SyntaxError: Cannot use import statement outside a module
You used import in a .js file that Node treats as CommonJS. A common cause: npm init -y now writes "type": "commonjs" into package.json. Change it to "module", or rename the file to .mjs.
“require is not defined in ES module scope”
ReferenceError: require is not defined in ES module scope, you can use import instead
You used require in an ES module. Switch to import:
// Wrong (in an ES module)
const fs = require('node:fs');
// Right
import fs from 'node:fs';
The same goes for __dirname is not defined in ES module scope: use import.meta.dirname.
Forgetting the extension
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/asha/shop/prices' imported from /Users/asha/shop/app.js
Did you mean to import "./prices.js"?
Node even suggests the fix. Write './prices.js', not './prices'.
Quick recap
- A module is a file that shares code through exports and imports.
- CommonJS uses
require()andmodule.exports. ES Modules useimportandexport. .mjsis always ESM,.cjsis always CommonJS, and.jsfollows"type"in the nearestpackage.json.- ESM needs file extensions in relative imports, supports top-level
await, runs in strict mode, and usesimport.meta.dirnameinstead of__dirname. - Use ES Modules for new code, with
"type": "module"inpackage.json.
// modules-demo.mjs: ES modules and CommonJS side by side
import { writeFile, mkdir } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import path from 'node:path';
import os from 'node:os';
// --- Create a tiny CommonJS module and an ES module in a temp folder
const dir = path.join(os.tmpdir(), 'modules-demo');
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, 'prices.cjs'), [
'const addTax = (amount) => amount * 1.2;',
'module.exports = { addTax, currency: "GBP" };',
].join('\n'));
await writeFile(path.join(dir, 'greet.mjs'), [
'export default function greet(name) { return `Hello, ${name}!`; }',
'export const shout = (text) => text.toUpperCase();',
].join('\n'));
// --- ES module: default and named imports (import() wants a file URL for full paths)
const greetUrl = pathToFileURL(path.join(dir, 'greet.mjs')).href;
const { default: greet, shout } = await import(greetUrl); // top-level await: ESM only
console.log(greet('Maya'), shout('sale today'));
// --- CommonJS from ESM, way 1: import() gives module.exports as "default"
const pricesUrl = pathToFileURL(path.join(dir, 'prices.cjs')).href;
const prices = (await import(pricesUrl)).default;
console.log('With tax:', prices.addTax(10), prices.currency);
// --- CommonJS from ESM, way 2: build a require() function
const require = createRequire(import.meta.url);
const again = require(path.join(dir, 'prices.cjs'));
console.log('Same object both ways?', again === prices);
// --- ESM has import.meta instead of __dirname and __filename
console.log('This folder:', import.meta.dirname);
console.log('typeof __dirname here:', typeof __dirname);
// --- ESM is always strict mode
try {
undeclaredTotal = 5;
} catch (error) {
console.log('Strict mode says:', error.message);
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready