Skip to main content

Chapter 7: JavaScript Modules - Complete Guide to ES6 Modules and Module Systems

Modules are a fundamental concept in modern JavaScript that enable code organization, reusability, and maintainability. ES6 modules provide a standardized way to structure JavaScript applications, making them more scalable and easier to maintain.

Why Modules Matter in JavaScript

Modules in JavaScript are essential because they:

  • Organize Code: Break large applications into manageable, focused units
  • Enable Reusability: Share code across different parts of an application
  • Prevent Global Pollution: Avoid conflicts with global variables
  • Support Tree Shaking: Enable bundlers to eliminate unused code
  • Facilitate Testing: Make individual components easier to test
  • Enable Modern Development: Support modern build tools and frameworks

Learning Objectives

Through this chapter, you will master:

  • ES6 module syntax and concepts
  • Import and export statements
  • Dynamic imports and code splitting
  • Module patterns and best practices
  • Module bundlers and build tools
  • Advanced module techniques

ES6 Module Basics

Export Statements

// math.js - Named exports
export const PI = 3.14159;
export const E = 2.71828;

export function add(a, b) {
return a + b;
}

export function subtract(a, b) {
return a - b;
}

export function multiply(a, b) {
return a * b;
}

export function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}

// Class export
export class Calculator {
constructor() {
this.history = [];
}

calculate(operation, a, b) {
const result = operation(a, b);
this.history.push({ operation: operation.name, a, b, result });
return result;
}

getHistory() {
return this.history;
}
}

// Default export
export default function createCalculator() {
return new Calculator();
}

Import Statements

// main.js - Importing from math.js

// Named imports
import { add, subtract, multiply, divide, PI, E } from './math.js';

// Default import
import createCalculator from './math.js';

// Class import
import { Calculator } from './math.js';

// Usage
console.log('PI:', PI);
console.log('E:', E);
console.log('2 + 3 =', add(2, 3));
console.log('10 - 4 =', subtract(10, 4));

// Using the class
const calc = new Calculator();
const result = calc.calculate(add, 5, 3);
console.log('Result:', result);
console.log('History:', calc.getHistory());

// Using default export
const defaultCalc = createCalculator();

Import Variations

// Import everything as a namespace
import * as MathUtils from './math.js';
console.log(MathUtils.add(1, 2));
console.log(MathUtils.PI);

// Rename imports
import { add as addition, subtract as subtraction } from './math.js';
console.log(addition(1, 2));
console.log(subtraction(5, 3));

// Import with renaming
import { Calculator as MathCalculator } from './math.js';
const calc = new MathCalculator();

// Mixed imports
import createCalculator, { add, subtract, PI } from './math.js';

// Re-export
export { add, subtract } from './math.js';
export { default as MathCalculator } from './math.js';

Module Patterns

Utility Module

// utils.js
export const formatDate = (date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};

export const formatCurrency = (amount, currency = 'USD') => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(amount);
};

export const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};

export const throttle = (func, limit) => {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
};

// Default export
export default {
formatDate,
formatCurrency,
debounce,
throttle
};

Configuration Module

// config.js
const config = {
api: {
baseUrl: process.env.NODE_ENV === 'production'
? 'https://api.production.com'
: 'https://api.development.com',
timeout: 5000,
retries: 3
},
ui: {
theme: 'light',
language: 'en',
animations: true
},
features: {
analytics: true,
notifications: true,
offlineMode: false
}
};

// Named exports for specific config sections
export const apiConfig = config.api;
export const uiConfig = config.ui;
export const featureConfig = config.features;

// Default export for entire config
export default config;

// Environment-specific configuration
export const getConfig = () => {
const env = process.env.NODE_ENV || 'development';

const configs = {
development: {
...config,
api: { ...config.api, timeout: 10000 }
},
production: {
...config,
features: { ...config.features, analytics: true }
},
test: {
...config,
api: { ...config.api, baseUrl: 'http://localhost:3000' }
}
};

return configs[env] || configs.development;
};

Service Module

// api.js
import { apiConfig } from './config.js';

class ApiService {
constructor() {
this.baseUrl = apiConfig.baseUrl;
this.timeout = apiConfig.timeout;
}

async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
timeout: this.timeout,
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};

try {
const response = await fetch(url, config);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;

return this.request(url, { method: 'GET' });
}

async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}

async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}

async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}

// Create singleton instance
const apiService = new ApiService();

// Export both class and instance
export { ApiService };
export default apiService;

Data Model Module

// models/User.js
export class User {
constructor(data = {}) {
this.id = data.id || null;
this.name = data.name || '';
this.email = data.email || '';
this.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
this.isActive = data.isActive !== undefined ? data.isActive : true;
}

// Getters
get displayName() {
return this.name || this.email.split('@')[0];
}

get isNew() {
return this.id === null;
}

// Methods
validate() {
const errors = [];

if (!this.name.trim()) {
errors.push('Name is required');
}

if (!this.email.trim()) {
errors.push('Email is required');
} else if (!this.isValidEmail(this.email)) {
errors.push('Email format is invalid');
}

return {
isValid: errors.length === 0,
errors
};
}

isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}

toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
createdAt: this.createdAt.toISOString(),
isActive: this.isActive
};
}

static fromJSON(data) {
return new User(data);
}

static createFromForm(formData) {
return new User({
name: formData.get('name'),
email: formData.get('email')
});
}
}

// Export factory function
export const createUser = (data) => new User(data);

// Export validation utilities
export const validateUserData = (data) => {
const user = new User(data);
return user.validate();
};

Dynamic Imports

Basic Dynamic Import

// Dynamic import - returns a promise
async function loadModule() {
try {
const module = await import('./math.js');
console.log('Module loaded:', module);
console.log('PI:', module.PI);
console.log('Add function:', module.add(2, 3));
} catch (error) {
console.error('Failed to load module:', error);
}
}

// Load module on demand
document.getElementById('load-math').addEventListener('click', loadModule);

Conditional Loading

// Conditional module loading
async function loadFeature(featureName) {
try {
let module;

switch (featureName) {
case 'analytics':
module = await import('./features/analytics.js');
break;
case 'notifications':
module = await import('./features/notifications.js');
break;
case 'offline':
module = await import('./features/offline.js');
break;
default:
throw new Error(`Unknown feature: ${featureName}`);
}

// Initialize the feature
if (module.default && typeof module.default.init === 'function') {
await module.default.init();
}

return module;
} catch (error) {
console.error(`Failed to load feature ${featureName}:`, error);
return null;
}
}

// Load features based on user preferences
async function initializeFeatures() {
const userPreferences = await getUserPreferences();

const featurePromises = userPreferences.enabledFeatures.map(feature =>
loadFeature(feature)
);

const loadedFeatures = await Promise.allSettled(featurePromises);

loadedFeatures.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Feature ${userPreferences.enabledFeatures[index]} loaded successfully`);
} else {
console.error(`Feature ${userPreferences.enabledFeatures[index]} failed to load:`, result.reason);
}
});
}

Code Splitting with Dynamic Imports

// Route-based code splitting
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
}

addRoute(path, modulePath) {
this.routes.set(path, modulePath);
}

async navigate(path) {
if (this.currentRoute === path) return;

const modulePath = this.routes.get(path);
if (!modulePath) {
throw new Error(`Route not found: ${path}`);
}

try {
// Show loading indicator
this.showLoading();

// Dynamically import the route module
const module = await import(modulePath);

// Initialize the route
if (module.default && typeof module.default.init === 'function') {
await module.default.init();
}

// Update current route
this.currentRoute = path;

// Hide loading indicator
this.hideLoading();

} catch (error) {
console.error(`Failed to load route ${path}:`, error);
this.hideLoading();
this.showError(`Failed to load page: ${path}`);
}
}

showLoading() {
document.getElementById('loading').style.display = 'block';
}

hideLoading() {
document.getElementById('loading').style.display = 'none';
}

showError(message) {
document.getElementById('error').textContent = message;
document.getElementById('error').style.display = 'block';
}
}

// Initialize router with routes
const router = new Router();
router.addRoute('/', './pages/home.js');
router.addRoute('/about', './pages/about.js');
router.addRoute('/contact', './pages/contact.js');
router.addRoute('/dashboard', './pages/dashboard.js');

// Handle navigation
document.addEventListener('click', (e) => {
if (e.target.matches('[data-route]')) {
e.preventDefault();
const route = e.target.dataset.route;
router.navigate(route);
}
});

Module Bundlers and Build Tools

Webpack Configuration

// webpack.config.js
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
resolve: {
extensions: ['.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
}
}
};

Vite Configuration

// vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils')
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'moment']
}
}
}
}
});

Advanced Module Techniques

Module Composition

// composables/useApi.js
import { ref, computed } from 'vue'; // Vue 3 Composition API example

export function useApi(baseUrl) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);

const isLoaded = computed(() => data.value !== null);
const hasError = computed(() => error.value !== null);

async function fetch(endpoint, options = {}) {
loading.value = true;
error.value = null;

try {
const response = await fetch(`${baseUrl}${endpoint}`, options);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}

function reset() {
data.value = null;
error.value = null;
loading.value = false;
}

return {
data,
loading,
error,
isLoaded,
hasError,
fetch,
reset
};
}

Plugin System

// plugins/PluginManager.js
class PluginManager {
constructor() {
this.plugins = new Map();
this.hooks = new Map();
}

register(name, plugin) {
if (this.plugins.has(name)) {
throw new Error(`Plugin ${name} is already registered`);
}

this.plugins.set(name, plugin);

// Initialize plugin if it has an init method
if (typeof plugin.init === 'function') {
plugin.init(this);
}

return this;
}

unregister(name) {
const plugin = this.plugins.get(name);

if (plugin && typeof plugin.destroy === 'function') {
plugin.destroy();
}

this.plugins.delete(name);
return this;
}

get(name) {
return this.plugins.get(name);
}

// Hook system
addHook(name, callback) {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name).push(callback);
}

removeHook(name, callback) {
const hooks = this.hooks.get(name);
if (hooks) {
const index = hooks.indexOf(callback);
if (index > -1) {
hooks.splice(index, 1);
}
}
}

async executeHook(name, ...args) {
const hooks = this.hooks.get(name) || [];
const results = [];

for (const hook of hooks) {
try {
const result = await hook(...args);
results.push(result);
} catch (error) {
console.error(`Hook ${name} failed:`, error);
}
}

return results;
}
}

// Example plugin
const analyticsPlugin = {
init(pluginManager) {
this.track('app_initialized');
pluginManager.addHook('user_action', this.track.bind(this));
},

track(event, data = {}) {
console.log('Analytics:', event, data);
// Send to analytics service
},

destroy() {
console.log('Analytics plugin destroyed');
}
};

// Usage
const pluginManager = new PluginManager();
pluginManager.register('analytics', analyticsPlugin);

Best Practices

1. Use Clear Module Structure

// Good: Clear, focused module
// userService.js
export class UserService {
constructor(apiClient) {
this.api = apiClient;
}

async getUser(id) {
return this.api.get(`/users/${id}`);
}

async createUser(userData) {
return this.api.post('/users', userData);
}
}

export default UserService;

// Avoid: Mixed responsibilities
// badModule.js
export function getUser(id) { /* ... */ }
export function formatDate(date) { /* ... */ }
export function validateEmail(email) { /* ... */ }
export function sendNotification(message) { /* ... */ }

2. Use Barrel Exports

// utils/index.js - Barrel export
export { formatDate, formatCurrency } from './formatters.js';
export { debounce, throttle } from './performance.js';
export { validateEmail, validatePhone } from './validators.js';

// Usage
import { formatDate, debounce, validateEmail } from './utils/index.js';

3. Handle Module Loading Errors

// Robust module loading
async function loadModuleSafely(modulePath, fallback = null) {
try {
const module = await import(modulePath);
return module;
} catch (error) {
console.error(`Failed to load module ${modulePath}:`, error);

if (fallback) {
console.log('Using fallback module');
return fallback;
}

throw error;
}
}

// Usage
const mathModule = await loadModuleSafely('./math.js', {
add: (a, b) => a + b,
subtract: (a, b) => a - b
});

Summary

JavaScript modules are essential for modern web development:

  • ES6 Modules: Use import/export for clean code organization
  • Dynamic Imports: Load modules on demand for better performance
  • Module Patterns: Create reusable, maintainable code structures
  • Build Tools: Use bundlers for optimization and code splitting
  • Best Practices: Follow clear structure and error handling patterns

Mastering modules enables you to build scalable, maintainable JavaScript applications that are easy to test, debug, and extend.


This tutorial is part of the JavaScript Mastery series by syscook.dev