ES Modules (import/export)
ES Modules (ESM) is the official JavaScript module standard, introduced in ES6 (2015). While CommonJS was created specifically for Node.js, ES Modules work in both browsers and Node.js, making it the future of JavaScript modules.
CommonJS vs ES Modules
| Feature | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Parsing | Runtime | Compile time |
| Top-level await | No | Yes |
| Browser support | No (needs bundler) | Yes (native) |
| Default in Node.js | Yes (.js files) | Needs configuration |
Enabling ES Modules in Node.js
There are two ways to use ES Modules:
Method 1: Use .mjs Extension
// math.mjs
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// app.mjs
import { add, subtract } from './math.mjs';
console.log(add(2, 3));
Method 2: Set "type": "module" in package.json
{
"name": "my-app",
"type": "module"
}
Now all .js files are treated as ES Modules:
// math.js (treated as ESM because of package.json)
export const add = (a, b) => a + b;
Named Exports
Export multiple values with their names:
// utils.js
export const PI = 3.14159;
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
export class Calculator {
add(a, b) { return a + b; }
}
// app.js
import { PI, square, cube, Calculator } from './utils.js';
console.log(PI); // 3.14159
console.log(square(4)); // 16
console.log(cube(3)); // 27
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
Default Exports
Export a single main value from a module:
// Logger.js
export default class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
error(message) {
console.error(`[ERROR] ${message}`);
}
}
// app.js
import Logger from './Logger.js'; // No curly braces!
const logger = new Logger();
logger.log('Application started');
You can name the default import anything:
import Logger from './Logger.js';
import MyLogger from './Logger.js'; // Same thing, different name
import Whatever from './Logger.js'; // Still works!
Combining Default and Named Exports
A module can have both:
// api.js
const API_VERSION = '2.0';
const BASE_URL = 'https://api.example.com';
function get(endpoint) {
return fetch(`${BASE_URL}${endpoint}`);
}
function post(endpoint, data) {
return fetch(`${BASE_URL}${endpoint}`, {
method: 'POST',
body: JSON.stringify(data)
});
}
// Default export
export default { get, post };
// Named exports
export { API_VERSION, BASE_URL };
// app.js
import api, { API_VERSION, BASE_URL } from './api.js';
console.log(`API ${API_VERSION} at ${BASE_URL}`);
api.get('/users');
Import Variations
Rename Imports
// Rename to avoid conflicts
import { square as sq, cube as cb } from './math.js';
console.log(sq(4)); // 16
console.log(cb(3)); // 27
Import All as Namespace
// Import everything as an object
import * as math from './math.js';
console.log(math.PI);
console.log(math.square(5));
console.log(math.default); // The default export, if any
Dynamic Imports
// Import at runtime (returns a Promise)
async function loadModule() {
const module = await import('./heavy-module.js');
module.doSomething();
}
// Useful for code splitting
if (needsAdvancedFeature) {
const { advancedFunction } = await import('./advanced.js');
advancedFunction();
}
Export Variations
Export List
// Define first, export later
const name = 'MyApp';
const version = '1.0.0';
function init() {
console.log(`${name} v${version} initialized`);
}
// Export at the end
export { name, version, init };
Re-export from Another Module
// index.js - re-export from other modules
// Re-export everything
export * from './math.js';
// Re-export specific items
export { UserService, AuthService } from './services.js';
// Re-export with rename
export { default as Config } from './config.js';
This pattern is common for creating barrel files (index.js) that combine exports.
Top-Level Await
ES Modules support await at the top level (without async function):
// config.js
const response = await fetch('/api/config');
const config = await response.json();
export default config;
// app.js
import config from './config.js'; // Waits for config to load
console.log(config.apiKey);
This is only available in ES Modules, not CommonJS.
Converting CommonJS to ES Modules
Interoperability
Using CommonJS in ES Modules
// app.mjs
import lodash from 'lodash'; // CommonJS package
import { readFileSync } from 'fs'; // Built-in works too
const result = lodash.chunk([1, 2, 3, 4], 2);
Using ES Modules in CommonJS
// app.js (CommonJS)
// Use dynamic import
async function main() {
const { default: myModule } = await import('./esm-module.mjs');
myModule.doSomething();
}
main();
Best Practices
1. Be Consistent
Choose one system per project. If using ES Modules, set "type": "module" in package.json.
2. Prefer Named Exports
Named exports make refactoring easier and enable better IDE support:
// Prefer this
export function processData(data) { }
export function validateData(data) { }
// Over this
export default {
processData(data) { },
validateData(data) { }
};
3. Use Default for Single Main Export
// Good use of default - one main class/function
export default class UserService { }
// Also export types/utilities as named
export const USER_ROLES = ['admin', 'user'];
4. Create Barrel Files
// src/utils/index.js
export { formatDate, parseDate } from './date.js';
export { formatCurrency } from './currency.js';
export { validateEmail, validatePhone } from './validation.js';
// Now import from one place
import { formatDate, formatCurrency, validateEmail } from './utils/index.js';
Key Takeaways
- ES Modules use
import/exportsyntax - Enable ESM with
.mjsextension or"type": "module"in package.json - Named exports use
export { name }orexport const name - Default exports use
export defaultand don't need curly braces on import - Dynamic imports (
import()) return Promises for code splitting - ESM supports top-level
await - Both systems can interoperate in Node.js
Summary
ES Modules represent the future of JavaScript modules. They offer static analysis (better tooling), async loading, and work across both browser and Node.js environments. While CommonJS is still widely used, new projects often prefer ES Modules for their standardization and modern features.
In the next lesson, we'll explore Node.js's built-in modules that provide powerful functionality out of the box.

