ES modules provide a standard way to organize and share JavaScript code. Here's how to use them effectively.
Basic Export and Import#
1// math.js - Named exports
2export const PI = 3.14159;
3
4export function add(a, b) {
5 return a + b;
6}
7
8export function multiply(a, b) {
9 return a * b;
10}
11
12// app.js - Named imports
13import { PI, add, multiply } from './math.js';
14
15console.log(PI); // 3.14159
16console.log(add(2, 3)); // 5
17console.log(multiply(2, 3)); // 6Default Exports#
1// user.js - Default export
2export default class User {
3 constructor(name) {
4 this.name = name;
5 }
6
7 greet() {
8 return `Hello, ${this.name}`;
9 }
10}
11
12// app.js - Import default
13import User from './user.js';
14
15const user = new User('John');
16user.greet();
17
18// Default with named exports
19export default function main() {}
20export const VERSION = '1.0.0';
21
22// Import both
23import main, { VERSION } from './module.js';Import Variations#
1// Rename on import
2import { add as sum, multiply as mul } from './math.js';
3sum(2, 3); // 5
4
5// Import all as namespace
6import * as math from './math.js';
7math.add(2, 3);
8math.PI;
9
10// Import default with namespace
11import User, * as utils from './module.js';
12
13// Side-effect import (just execute)
14import './polyfills.js';
15import './styles.css'; // With bundlers
16
17// Import assertions (JSON)
18import data from './data.json' with { type: 'json' };Export Variations#
1// Inline export
2export const name = 'John';
3export function greet() {}
4export class User {}
5
6// Export list
7const name = 'John';
8function greet() {}
9class User {}
10
11export { name, greet, User };
12
13// Rename on export
14export { name as userName, greet as sayHello };
15
16// Re-export from another module
17export { add, multiply } from './math.js';
18export * from './utils.js';
19export * as math from './math.js';
20
21// Re-export default
22export { default } from './module.js';
23export { default as User } from './user.js';Dynamic Imports#
1// Static import (top-level, hoisted)
2import { add } from './math.js';
3
4// Dynamic import (returns Promise)
5async function loadModule() {
6 const math = await import('./math.js');
7 math.add(2, 3);
8}
9
10// Conditional loading
11if (condition) {
12 const { feature } = await import('./feature.js');
13 feature();
14}
15
16// Lazy loading with routes
17const routes = {
18 '/': () => import('./pages/home.js'),
19 '/about': () => import('./pages/about.js'),
20 '/contact': () => import('./pages/contact.js')
21};
22
23async function loadPage(path) {
24 const loader = routes[path];
25 if (loader) {
26 const module = await loader();
27 module.render();
28 }
29}
30
31// Error handling
32try {
33 const module = await import('./optional-feature.js');
34 module.init();
35} catch (err) {
36 console.log('Feature not available');
37}Module Patterns#
1// Barrel exports (index.js)
2// components/index.js
3export { Button } from './Button.js';
4export { Input } from './Input.js';
5export { Modal } from './Modal.js';
6export * from './utils.js';
7
8// Import from barrel
9import { Button, Input, Modal } from './components/index.js';
10// Or: import { Button } from './components'; // With bundler
11
12// Factory pattern
13// logger.js
14function createLogger(prefix) {
15 return {
16 log: (msg) => console.log(`[${prefix}] ${msg}`),
17 error: (msg) => console.error(`[${prefix}] ${msg}`)
18 };
19}
20
21export default createLogger;
22
23// Singleton pattern
24// config.js
25class Config {
26 constructor() {
27 this.settings = {};
28 }
29
30 set(key, value) {
31 this.settings[key] = value;
32 }
33
34 get(key) {
35 return this.settings[key];
36 }
37}
38
39export default new Config(); // Single instance exportedCircular Dependencies#
1// a.js
2import { b } from './b.js';
3export const a = 'A';
4console.log('a:', b); // Might be undefined!
5
6// b.js
7import { a } from './a.js';
8export const b = 'B';
9console.log('b:', a); // Might be undefined!
10
11// Solution 1: Use functions (deferred access)
12// a.js
13import { getB } from './b.js';
14export const a = 'A';
15export function getA() { return a; }
16console.log('a:', getB());
17
18// b.js
19import { getA } from './a.js';
20export const b = 'B';
21export function getB() { return b; }
22console.log('b:', getA());
23
24// Solution 2: Restructure to avoid cycles
25// shared.js
26export const shared = {};
27
28// a.js
29import { shared } from './shared.js';
30shared.a = 'A';
31
32// b.js
33import { shared } from './shared.js';
34shared.b = 'B';Module Scope#
1// Each module has its own scope
2// module-a.js
3const secret = 'hidden';
4export const visible = 'public';
5
6// module-b.js
7import { visible } from './module-a.js';
8// secret is not accessible
9
10// Top-level await (in modules)
11const data = await fetch('/api/config').then(r => r.json());
12export default data;
13
14// Module-level this
15console.log(this); // undefined (not window)
16
17// import.meta
18console.log(import.meta.url); // Module's URL
19// In Node.js
20import.meta.dirname // Directory path
21import.meta.filename // File pathNode.js Specifics#
1// package.json
2{
3 "type": "module" // Enable ES modules
4}
5
6// Or use .mjs extension
7// math.mjs
8
9// CommonJS interop
10import fs from 'fs'; // Works with Node built-ins
11import pkg from './package.json' with { type: 'json' };
12
13// Named imports from CommonJS (sometimes works)
14import { readFile } from 'fs';
15
16// Default import always works
17import lodash from 'lodash';
18
19// Import CommonJS as namespace
20import * as _ from 'lodash';
21
22// __dirname and __filename alternatives
23import { fileURLToPath } from 'url';
24import { dirname } from 'path';
25
26const __filename = fileURLToPath(import.meta.url);
27const __dirname = dirname(__filename);Browser Usage#
1<!-- ES module in browser -->
2<script type="module" src="app.js"></script>
3
4<!-- Inline module -->
5<script type="module">
6 import { greet } from './greet.js';
7 greet('World');
8</script>
9
10<!-- Fallback for older browsers -->
11<script nomodule src="legacy-bundle.js"></script>
12
13<!-- Import maps -->
14<script type="importmap">
15{
16 "imports": {
17 "lodash": "https://cdn.skypack.dev/lodash",
18 "@utils/": "./src/utils/"
19 }
20}
21</script>
22
23<script type="module">
24 import _ from 'lodash';
25 import { helper } from '@utils/helper.js';
26</script>Module Design#
1// Keep exports focused
2// ❌ Too many exports
3export { a, b, c, d, e, f, g, h, i, j };
4
5// ✓ Grouped with namespace or barrel
6import * as validators from './validators/index.js';
7
8// Prefer named exports for utilities
9export function formatDate(date) {}
10export function formatCurrency(amount) {}
11
12// Use default for main class/function
13export default class UserService {}
14
15// Export types alongside values (TypeScript)
16export interface User {
17 id: number;
18 name: string;
19}
20
21export function createUser(name: string): User {
22 return { id: Date.now(), name };
23}Tree Shaking#
1// Tree shaking removes unused exports
2// utils.js
3export function used() {
4 return 'This is used';
5}
6
7export function unused() {
8 return 'This will be removed';
9}
10
11// app.js
12import { used } from './utils.js';
13// unused() is removed from bundle
14
15// Side effects prevent tree shaking
16// ❌ Has side effects
17let cache = {};
18export function getData() {
19 return cache;
20}
21
22// ✓ Mark as side-effect free
23// package.json
24{
25 "sideEffects": false
26}
27
28// Or list files with side effects
29{
30 "sideEffects": ["./src/polyfills.js", "*.css"]
31}Testing with Modules#
1// Easy to test with ES modules
2// calculator.js
3export function add(a, b) {
4 return a + b;
5}
6
7// calculator.test.js
8import { add } from './calculator.js';
9
10test('add', () => {
11 expect(add(2, 3)).toBe(5);
12});
13
14// Mock modules
15jest.mock('./api.js', () => ({
16 fetchUser: jest.fn(() => Promise.resolve({ name: 'Mock' }))
17}));
18
19import { fetchUser } from './api.js';Best Practices#
Export Design:
✓ One module = one responsibility
✓ Named exports for utilities
✓ Default export for main item
✓ Use barrel files for public API
Import Organization:
✓ External packages first
✓ Internal absolute paths
✓ Relative paths last
✓ Group by type
File Structure:
✓ index.js for public API
✓ Consistent naming
✓ Colocate related code
✓ Avoid deep nesting
Avoid:
✗ Circular dependencies
✗ Wildcard re-exports (*)
✗ Importing from deep paths
✗ Side effects in modules
Conclusion#
ES modules are the standard for JavaScript code organization. Use named exports for utilities and default exports for main functionality. Leverage dynamic imports for code splitting, barrel files for clean public APIs, and import maps for browser deployment. Keep modules focused, avoid circular dependencies, and design for tree shaking to create maintainable, efficient code.