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:

  1. Use .mjs file extension, OR
  2. Set "type": "module" in package.json (then all .js files 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 import CJS modules (default import only)
  • CJS can use await import() to load ESM modules (dynamic import)
  • CJS cannot use static import syntax

🏠 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

codeTap to expand ⛶
1// ES Modules — Complete Guide
2
3// === math.mjs (or .js with "type": "module" in package.json) ===
4
5// Named exports
6export const PI = 3.14159;
7export const E = 2.71828;
8
9export function add(a, b) {
10 return a + b;
11}
12
13export function subtract(a, b) {
14 return a - b;
15}
16
17// Default export (one per module)
18export default class Calculator {
19 constructor() {
20 this.history = [];
21 }
22
23 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 }
35
36 getHistory() {
37 return [...this.history];
38 }
39}
40
41// === app.mjs — Importing ===
42
43// 1. Import named exports
44import { add, subtract, PI } from "./math.mjs";
45console.log(add(2, 3)); // 5
46console.log(PI); // 3.14159
47
48// 2. Import default export
49import Calculator from "./math.mjs";
50const calc = new Calculator();
51console.log(calc.calculate("+", 10, 5)); // 15
52
53// 3. Import everything as a namespace
54import * as MathUtils from "./math.mjs";
55console.log(MathUtils.add(1, 2)); // 3
56console.log(MathUtils.PI); // 3.14159
57
58// 4. Rename imports (aliasing)
59import { add as sum, subtract as diff } from "./math.mjs";
60console.log(sum(1, 2)); // 3
61console.log(diff(5, 3)); // 2
62
63// 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";
68
69// 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}
79
80// 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 resolves
84
85// 8. import.meta — Module metadata
86console.log("Module URL:", import.meta.url);
87// file:///Users/you/project/app.mjs
88
89// Get __dirname equivalent in ESM
90import { fileURLToPath } from "url";
91import { dirname } from "path";
92const __filename = fileURLToPath(import.meta.url);
93const __dirname = dirname(__filename);
94console.log("Directory:", __dirname);
95
96// 9. Importing CommonJS from ESM
97// ✅ This works — CJS default export becomes the ESM default
98// import express from "express"; // express is CJS
99
100// ❌ Named imports from CJS may not work
101// import { Router } from "express"; // May fail!
102// Instead:
103// import express from "express";
104// const { Router } = express;
105
106// 10. Conditional exports in package.json
107// {
108// "exports": {
109// ".": {
110// "import": "./dist/index.mjs", // ESM entry
111// "require": "./dist/index.cjs" // CJS entry
112// }
113// }
114// }

🏋️ Practice Exercise

Exercises:

  1. Convert a CommonJS project to ES Modules — change require to import, module.exports to export
  2. Create a barrel export pattern with an index.mjs that re-exports from 3+ sub-modules
  3. Use dynamic import() to build a plugin system that loads modules based on configuration
  4. Write a module using top-level await to load configuration from a JSON file
  5. Create a dual-format package that works with both CJS (require) and ESM (import)
  6. Use import.meta.url to 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 use import { add } from './math.mjs' or './math.js'

  • Trying to use require() in an ESM file — ESM has import syntax and dynamic import(); require is not available

  • Expecting 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 why import syntax throws SyntaxError in .js files

  • Using 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.