Back to Blog
JavaScriptModulesES6Bundling

JavaScript Modules Explained

Master JavaScript modules. From ES modules to CommonJS to dynamic imports and bundling.

B
Bootspring Team
Engineering
September 22, 2020
7 min read

Modules organize code into reusable pieces. Here's how they work in JavaScript.

ES Modules (ESM)#

1// Named exports 2// math.js 3export const PI = 3.14159; 4 5export function add(a, b) { 6 return a + b; 7} 8 9export function multiply(a, b) { 10 return a * b; 11} 12 13// Named imports 14// app.js 15import { add, multiply, PI } from './math.js'; 16 17console.log(add(2, 3)); // 5 18console.log(PI); // 3.14159 19 20// Rename imports 21import { add as sum } from './math.js'; 22 23// Import all as namespace 24import * as math from './math.js'; 25console.log(math.add(2, 3)); 26console.log(math.PI);

Default Exports#

1// Default export 2// calculator.js 3export default class Calculator { 4 add(a, b) { 5 return a + b; 6 } 7 8 subtract(a, b) { 9 return a - b; 10 } 11} 12 13// Default import (any name works) 14// app.js 15import Calculator from './calculator.js'; 16import MyCalc from './calculator.js'; // Same thing 17 18const calc = new Calculator(); 19 20// Default + named exports 21// utils.js 22export default function main() { 23 console.log('Main function'); 24} 25 26export function helper() { 27 console.log('Helper function'); 28} 29 30// Import both 31import main, { helper } from './utils.js';

Re-exporting#

1// Aggregate exports from multiple modules 2// index.js 3export { add, multiply } from './math.js'; 4export { default as Calculator } from './calculator.js'; 5export * from './helpers.js'; 6 7// Rename while re-exporting 8export { add as sum } from './math.js'; 9 10// Re-export default 11export { default } from './calculator.js'; 12 13// Re-export all with namespace 14export * as math from './math.js';

CommonJS (Node.js)#

1// module.exports 2// math.js 3const PI = 3.14159; 4 5function add(a, b) { 6 return a + b; 7} 8 9module.exports = { 10 PI, 11 add, 12}; 13 14// Or export directly 15module.exports.subtract = function(a, b) { 16 return a - b; 17}; 18 19// require 20// app.js 21const { add, PI } = require('./math.js'); 22const math = require('./math.js'); 23 24console.log(math.add(2, 3)); 25 26// Default-like export 27// calculator.js 28class Calculator {} 29module.exports = Calculator; 30 31// Import 32const Calculator = require('./calculator.js');

Dynamic Imports#

1// Dynamic import returns a promise 2async function loadModule() { 3 const math = await import('./math.js'); 4 console.log(math.add(2, 3)); 5} 6 7// Conditional loading 8async function loadFeature(name) { 9 if (name === 'charts') { 10 const { Chart } = await import('./charts.js'); 11 return new Chart(); 12 } 13 if (name === 'maps') { 14 const { Map } = await import('./maps.js'); 15 return new Map(); 16 } 17} 18 19// Lazy loading components (React) 20const LazyComponent = React.lazy(() => import('./HeavyComponent.js')); 21 22// With error handling 23async function safeImport(path) { 24 try { 25 return await import(path); 26 } catch (error) { 27 console.error(`Failed to load module: ${path}`, error); 28 return null; 29 } 30} 31 32// Preloading 33function preloadModule(path) { 34 return import(path); 35} 36 37// Preload on hover 38element.addEventListener('mouseenter', () => { 39 preloadModule('./feature.js'); 40});

Import Meta#

1// Module metadata 2console.log(import.meta.url); 3// file:///path/to/module.js 4 5// Get directory name (ESM) 6const __dirname = new URL('.', import.meta.url).pathname; 7 8// Environment variables (Vite) 9console.log(import.meta.env.MODE); 10console.log(import.meta.env.VITE_API_URL); 11 12// Hot module replacement (Vite) 13if (import.meta.hot) { 14 import.meta.hot.accept((newModule) => { 15 // Handle update 16 }); 17}

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'; 6 7// Import from barrel 8import { Button, Input, Modal } from './components'; 9 10// Namespace pattern 11// api/index.js 12import * as users from './users.js'; 13import * as posts from './posts.js'; 14import * as comments from './comments.js'; 15 16export const api = { 17 users, 18 posts, 19 comments, 20}; 21 22// Usage 23import { api } from './api'; 24api.users.getAll(); 25 26// Factory pattern 27// logger.js 28export function createLogger(prefix) { 29 return { 30 log: (msg) => console.log(`[${prefix}] ${msg}`), 31 error: (msg) => console.error(`[${prefix}] ${msg}`), 32 }; 33} 34 35// Usage 36import { createLogger } from './logger.js'; 37const logger = createLogger('App');

Circular Dependencies#

1// Avoid circular dependencies! 2 3// BAD: Circular dependency 4// a.js 5import { b } from './b.js'; 6export const a = 'A' + b; 7 8// b.js 9import { a } from './a.js'; 10export const b = 'B' + a; // a is undefined! 11 12// SOLUTION 1: Restructure 13// shared.js 14export const shared = 'shared'; 15 16// a.js 17import { shared } from './shared.js'; 18export const a = 'A' + shared; 19 20// b.js 21import { shared } from './shared.js'; 22export const b = 'B' + shared; 23 24// SOLUTION 2: Lazy access 25// a.js 26export function getA() { 27 const { b } = require('./b.js'); 28 return 'A' + b; 29} 30 31// SOLUTION 3: Dependency injection 32// a.js 33export function createA(b) { 34 return 'A' + b; 35}

Browser vs Node.js#

1<!-- Browser: type="module" --> 2<script type="module"> 3 import { add } from './math.js'; 4 console.log(add(2, 3)); 5</script> 6 7<!-- Or with src --> 8<script type="module" src="./app.js"></script> 9 10<!-- Fallback for older browsers --> 11<script nomodule src="./legacy-bundle.js"></script>
1// Node.js: .mjs extension or package.json 2// package.json 3{ 4 "type": "module" 5} 6 7// Or use .mjs extension 8// math.mjs 9export function add(a, b) { 10 return a + b; 11} 12 13// Import in Node.js 14import { add } from './math.mjs'; 15// or 16import { add } from './math.js'; // with "type": "module" 17 18// Interop: ESM importing CommonJS 19import pkg from './cjs-module.cjs'; 20// or 21import { createRequire } from 'module'; 22const require = createRequire(import.meta.url); 23const pkg = require('./cjs-module.js');

TypeScript Modules#

1// Export types 2export interface User { 3 id: number; 4 name: string; 5} 6 7export type UserRole = 'admin' | 'user' | 'guest'; 8 9// Type-only exports 10export type { User, UserRole }; 11 12// Type-only imports 13import type { User } from './types'; 14 15// Importing JSON (with resolveJsonModule) 16import config from './config.json'; 17 18// Declare module augmentation 19declare module 'express' { 20 interface Request { 21 user?: User; 22 } 23}

Module Resolution#

1// Relative imports 2import { add } from './math.js'; 3import { add } from '../utils/math.js'; 4 5// Bare imports (node_modules) 6import React from 'react'; 7import { useState } from 'react'; 8 9// Absolute imports (with configuration) 10import { add } from '@/utils/math'; 11import { Button } from '@components/Button'; 12 13// tsconfig.json or jsconfig.json 14{ 15 "compilerOptions": { 16 "baseUrl": ".", 17 "paths": { 18 "@/*": ["src/*"], 19 "@components/*": ["src/components/*"] 20 } 21 } 22} 23 24// Vite configuration 25// vite.config.js 26export default { 27 resolve: { 28 alias: { 29 '@': '/src', 30 '@components': '/src/components', 31 }, 32 }, 33};

Tree Shaking#

1// Tree shaking removes unused exports 2 3// math.js 4export function add(a, b) { 5 return a + b; 6} 7 8export function multiply(a, b) { 9 return a * b; // Not imported, will be removed 10} 11 12// app.js 13import { add } from './math.js'; 14// multiply is tree-shaken away 15 16// Ensure tree-shakeable code 17// DON'T: Side effects at module level 18let cache = []; 19export function getCache() { 20 return cache; 21} 22 23// DO: Pure exports 24export function createCache() { 25 return []; 26} 27 28// Mark side-effect-free in package.json 29{ 30 "sideEffects": false 31} 32// Or specify which files have side effects 33{ 34 "sideEffects": ["*.css", "*.scss"] 35}

Best Practices#

Organization: ✓ One export per file for large exports ✓ Use barrel files (index.js) for folders ✓ Keep related code together ✓ Avoid deep nesting Naming: ✓ Use descriptive file names ✓ Match export name to file name ✓ Use camelCase for functions ✓ Use PascalCase for classes/components Imports: ✓ Group imports logically ✓ External packages first ✓ Then internal modules ✓ Use absolute imports for clarity Avoid: ✗ Circular dependencies ✗ Side effects in modules ✗ Mixing ESM and CommonJS ✗ Default exports for utilities

Conclusion#

JavaScript modules enable clean code organization and reusability. Use ES modules for modern projects, understand CommonJS for Node.js compatibility, and leverage dynamic imports for code splitting. Keep modules focused, avoid circular dependencies, and structure imports for tree shaking.

Share this article

Help spread the word about Bootspring