ES Modules (import/export)
📖 Concept
ES Modules (ESM) is the official JavaScript module standard, supported natively in Node.js since v12. It's the future of JavaScript modules, offering static analysis, tree-shaking, and top-level await.
Enabling ESM in Node.js:
- Use
.mjsfile extension, OR - Set
"type": "module"inpackage.json(then all.jsfiles are ESM)
Key differences from CommonJS:
| Feature | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports |
import / export |
| Loading | Synchronous | Asynchronous |
| Parsing | Dynamic (runtime) | Static (parse time) |
| Top-level await | ❌ Not supported | ✅ Supported |
| Tree-shaking | ❌ Not possible | ✅ Bundlers can eliminate dead code |
__filename / __dirname |
✅ Available | ❌ Use import.meta.url |
| Conditional imports | ✅ require() anywhere |
❌ import must be at top level |
| Dynamic imports | require() |
import() (returns Promise) |
| Default + Named | Awkward pattern | First-class support |
| File extensions | Optional | Required for relative imports |
Static analysis advantage:
Because ESM import statements are analyzed at parse time (before execution), bundlers like webpack, Rollup, and esbuild can:
- Determine the entire dependency graph without running code
- Tree-shake unused exports (dead code elimination)
- Detect import errors at build time, not runtime
Interop between CJS and ESM:
- ESM can
importCJS modules (default import only) - CJS can use
await import()to load ESM modules (dynamic import) - CJS cannot use static
importsyntax
🏠 Real-world analogy: If CommonJS is like ordering from a catalog over the phone (you ask for items one by one, synchronously), ESM is like online shopping with a cart (you add everything you need, and the system optimizes the delivery).
💻 Code Example
1// ES Modules — Complete Guide23// === math.mjs (or .js with "type": "module" in package.json) ===45// Named exports6export const PI = 3.14159;7export const E = 2.71828;89export function add(a, b) {10 return a + b;11}1213export function subtract(a, b) {14 return a - b;15}1617// Default export (one per module)18export default class Calculator {19 constructor() {20 this.history = [];21 }2223 calculate(op, a, b) {24 let result;25 switch (op) {26 case "+": result = a + b; break;27 case "-": result = a - b; break;28 case "*": result = a * b; break;29 case "/": result = b !== 0 ? a / b : NaN; break;30 default: throw new Error(`Unknown operator: ${op}`);31 }32 this.history.push({ op, a, b, result });33 return result;34 }3536 getHistory() {37 return [...this.history];38 }39}4041// === app.mjs — Importing ===4243// 1. Import named exports44import { add, subtract, PI } from "./math.mjs";45console.log(add(2, 3)); // 546console.log(PI); // 3.141594748// 2. Import default export49import Calculator from "./math.mjs";50const calc = new Calculator();51console.log(calc.calculate("+", 10, 5)); // 155253// 3. Import everything as a namespace54import * as MathUtils from "./math.mjs";55console.log(MathUtils.add(1, 2)); // 356console.log(MathUtils.PI); // 3.141595758// 4. Rename imports (aliasing)59import { add as sum, subtract as diff } from "./math.mjs";60console.log(sum(1, 2)); // 361console.log(diff(5, 3)); // 26263// 5. Re-exporting (barrel exports)64// === index.mjs ===65// export { add, subtract } from "./math.mjs";66// export { default as Calculator } from "./math.mjs";67// export * from "./utils.mjs";6869// 6. Dynamic imports (async, works anywhere)70async function loadModule(moduleName) {71 try {72 const module = await import(`./modules/${moduleName}.mjs`);73 return module.default || module;74 } catch (err) {75 console.error(`Failed to load module: ${moduleName}`);76 return null;77 }78}7980// 7. Top-level await (ESM only!)81// const data = await fs.promises.readFile("config.json", "utf-8");82// const config = JSON.parse(data);83// This blocks the module from finishing loading until the await resolves8485// 8. import.meta — Module metadata86console.log("Module URL:", import.meta.url);87// file:///Users/you/project/app.mjs8889// Get __dirname equivalent in ESM90import { fileURLToPath } from "url";91import { dirname } from "path";92const __filename = fileURLToPath(import.meta.url);93const __dirname = dirname(__filename);94console.log("Directory:", __dirname);9596// 9. Importing CommonJS from ESM97// ✅ This works — CJS default export becomes the ESM default98// import express from "express"; // express is CJS99100// ❌ Named imports from CJS may not work101// import { Router } from "express"; // May fail!102// Instead:103// import express from "express";104// const { Router } = express;105106// 10. Conditional exports in package.json107// {108// "exports": {109// ".": {110// "import": "./dist/index.mjs", // ESM entry111// "require": "./dist/index.cjs" // CJS entry112// }113// }114// }
🏋️ Practice Exercise
Exercises:
- Convert a CommonJS project to ES Modules — change
requiretoimport,module.exportstoexport - Create a barrel export pattern with an
index.mjsthat re-exports from 3+ sub-modules - Use dynamic
import()to build a plugin system that loads modules based on configuration - Write a module using top-level
awaitto load configuration from a JSON file - Create a dual-format package that works with both CJS (
require) and ESM (import) - Use
import.meta.urlto read a file relative to the current module's location
⚠️ Common Mistakes
Forgetting file extensions in ESM imports —
import { add } from './math'fails; you MUST useimport { add } from './math.mjs'or'./math.js'Trying to use
require()in an ESM file — ESM hasimportsyntax and dynamicimport();requireis not availableExpecting named imports from CommonJS modules — CJS exports become a single default export in ESM; destructure after the default import
Not setting
"type": "module"in package.json and wondering whyimportsyntax throws SyntaxError in.jsfilesUsing top-level await without understanding it blocks the entire module graph — dependent modules wait for the await to resolve before they can execute
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for ES Modules (import/export). Login to unlock this feature.