Back to Blog
JavaScriptModulesES6Import

JavaScript Modules Guide

Master JavaScript ES modules for organizing and sharing code.

B
Bootspring Team
Engineering
September 1, 2018
7 min read

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)); // 6

Default 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 exported

Circular 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 path

Node.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.

Share this article

Help spread the word about Bootspring