Hey there, fellow code wranglers!
Ever been knee-deep in a JavaScript project, humming along, and then BAM! You hit a file using require() and another using import, and your brain does a little “system overload” dance? Or maybe you’ve tried exporting something and wondered if it should be module.exports, exports.thing, or export default? Yeah, you’re not alone in this beautiful chaos that is JavaScript modules.
It feels like a perennial “pick a team” debate: CommonJS vs. ECMAScript Modules (ESM). Let’s unwrap this present of perplexity and see if we can make sense of it all. No boxing gloves required, just a cup of your favorite beverage and maybe a few brain cells.
CommonJS: The OG (Original Gangster) of Node.js
Before JavaScript had a standardized module system for the browser or server, Node.js introduced CommonJS. It was (and still is) a fantastic way to organize your server-side code, breaking it down into manageable, reusable chunks.
Think of CommonJS as that reliable old car that gets you where you need to go without much fuss. It loads modules synchronously, meaning it waits for each module to load before moving on. Great for server-side where files are local!
How it Works in CommonJS:
- Importing: You use
require(). It takes the path to the module you want to load. - Exporting: You use
module.exportsorexports.module.exportsis what’s truly exported, whileexportsis just a shortcut tomodule.exportsif you’re adding multiple properties.
Let’s look at an example:
utils.js (The module to be exported):
// This is CommonJS territory!
function add(a, b) {
return a + b;
}
const PI = 3.14159;
// We export an object containing our functions/variables
module.exports = {
addNumbers: add,
circumferenceFactor: PI * 2,
appName: "My Vintage Calculator"
};
// You can also add properties directly to 'exports'
// exports.subtract = (a, b) => a - b;
// If you do this, 'module.exports' would be the initial value of 'exports' plus 'subtract'.
// But sticking to 'module.exports' for the main export is generally clearer.
app.js (The file importing the module):
// Again, CommonJS!
const { addNumbers, circumferenceFactor, appName } = require('./utils');
console.log(`Welcome to ${appName}!`);
const sum = addNumbers(10, 5);
console.log(`10 + 5 = ${sum}`);
console.log(`Circumference factor: ${circumferenceFactor}`);
// If utils.js also had exports.subtract, you could do:
// const utils = require('./utils');
// console.log(`10 - 5 = ${utils.subtract(10, 5)}`);
To run this, you’d simply run node app.js. Simple, right?
ECMAScript Modules (ESM): The Modern, Standardized Approach
Then came along the “standard.” ECMAScript Modules (ESM) were designed to be the official, spec-compliant module system for JavaScript, born primarily for browsers but eventually making their way into Node.js.
ESM uses import and export statements. Unlike CommonJS, ESM is designed for asynchronous loading, which is super efficient for things like “tree-shaking” (removing unused code during bundling) and parallel loading in browsers.
How it Works in ESM:
- Importing: You use the
importkeyword. - Exporting: You use the
exportkeyword. There are two main types:- Named Exports: You export multiple values by their names. You must
importthem using the exact same names (or an alias). - Default Exports: You can only have one default export per module. You can import it with any name you like.
- Named Exports: You export multiple values by their names. You must
Let’s update our example to ESM. For Node.js to treat files as ESM, you generally either name them with a .mjs extension or add "type": "module" to your package.json.
utils.mjs (Our module, now in ESM!):
// Hello, ESM!
export function multiply(a, b) {
return a * b;
}
export const GREETING = "Hello from ESM!";
// You can also have a default export
function divide(a, b) {
if (b === 0) throw new Error("Can't divide by zero!");
return a / b;
}
export default divide; // Only one default export per module!
app.mjs (The file importing ESM goodness):
// This file is also ESM, thanks to .mjs extension or package.json "type": "module"
// Importing named exports:
import { multiply, GREETING } from './utils.mjs';
// Importing the default export (we can name it whatever we want!):
import customDivider from './utils.mjs';
// If you have both named and default, you can combine:
// import customDivider, { multiply, GREETING } from './utils.mjs';
console.log(GREETING);
console.log(`5 * 3 = ${multiply(5, 3)}`);
try {
console.log(`10 / 2 = ${customDivider(10, 2)}`);
console.log(`10 / 0 = ${customDivider(10, 0)}`); // This will throw an error
} catch (error) {
console.error(`Oops: ${error.message}`);
}
To run this, you’d use node app.mjs.
The “Confusion”: Why Can’t We All Just Get Along?
So, why the brain drain? Because for a while, Node.js was CommonJS, and browsers were effectively moving towards ESM via bundlers like Webpack. This led to a bifurcated world.
Node.js eventually adopted ESM, but now you have to decide which system your project uses. Mixing them directly without build tools (like Babel or TypeScript, or bundlers) can lead to headaches, as require() doesn’t understand export syntax, and import doesn’t natively understand module.exports.
It’s not really about “picking a permanent team” so much as “understanding the playing field.”
- New Node.js projects? Leaning towards ESM is the modern way. You get tree-shaking benefits and align with browser standards. Remember your
.mjsorpackage.json"type": "module". - Older Node.js projects/legacy code? CommonJS is your friend. Don’t go refactoring for the sake of it unless there’s a strong reason.
- Front-end development? You’re almost always using ESM, even if you don’t realize it. Your bundler (Webpack, Rollup, Vite, Parcel) takes your ESM code and optimizes it for the browser.
Wrapping Up: From Chaos to Clarity
At the end of the day, both CommonJS and ESM are powerful tools for organizing your JavaScript code.
- CommonJS: Synchronous,
require(),module.exports. The classic Node.js way. - ESM: Asynchronous-friendly,
import/export(named and default). The modern, standardized way for both browser and Node.js.
The key is to understand which system your project is using and why. Once you grasp the simple syntax differences and the underlying philosophy (synchronous vs. asynchronous, named vs. default), that “confusion” melts away. You’ll stop feeling like you’re caught in a module tug-of-war and start wielding both require and import like the pro you are!
Happy coding!