Before modules, all JavaScript code shared one global scope — which was a nightmare for large projects. Modules let us split code into separate files, each with its own scope, and explicitly share only what we want.
Named Exports and Imports
We can export multiple things from a file by name. When importing, we use the exact same names (inside curly braces).
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// app.js
import { PI, add, multiply } from './math.js';
console.log(add(2, 3)); // 5
We can also rename imports if there’s a name collision:
import { add as sum } from './math.js';
console.log(sum(2, 3)); // 5
Default Exports and Imports
Each file can have one default export. When importing a default, we don’t use curly braces and we can name it anything we want.
// logger.js
export default function log(msg) {
console.log(`[LOG] ${msg}`);
}
// app.js — we can call it whatever we want
import log from './logger.js';
import myLogger from './logger.js'; // this works too
We can mix default and named exports in the same file, though it’s generally cleaner to pick one style.
// utils.js
export default function main() { /* ... */ }
export const VERSION = '1.0';
// app.js
import main, { VERSION } from './utils.js';
Re-exporting
When building a library or module with multiple files, we can re-export from an index file to create a clean public API.
// components/index.js
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { default as Card } from './Card.js';
// app.js — clean single import
import { Button, Modal, Card } from './components/index.js';
Dynamic Imports
Sometimes we don’t want to load a module until we actually need it. import() returns a Promise, so we can use it with await or .then().
// Load a heavy module only when the user clicks
button.addEventListener('click', async () => {
const { Chart } = await import('./chart.js');
const chart = new Chart('#canvas');
chart.render(data);
});
This is great for code splitting — the browser doesn’t download the module until it’s needed, making the initial page load faster.
CommonJS vs ES Modules
This is a common interview question. CommonJS (require) is the old Node.js way. ES Modules (import/export) is the standard that works in both browsers and modern Node.js.
// CommonJS (Node.js traditional way)
const fs = require('fs');
module.exports = { myFunction };
module.exports = myFunction; // default-like export
// ES Modules (modern standard)
import fs from 'fs';
export { myFunction };
export default myFunction;
Key differences:
- CommonJS loads modules synchronously at runtime.
requirecan be called anywhere, even insideifblocks. - ES Modules are statically analyzed at build time.
importmust be at the top level (except dynamicimport()). - CommonJS uses
require()/module.exports. ES Modules useimport/export. - In Node.js, use
.mjsextension or set"type": "module"inpackage.jsonto use ES Modules.
In simple language, if we’re writing modern JavaScript (whether for the browser or Node.js), we should use ES Modules. CommonJS still works in Node.js but ES Modules are the future.