Introduction
The Module pattern encapsulates code into self-contained units with public and private members. Before ES6 modules, this was the primary way to avoid polluting the global namespace and create private state.
Key Concepts
IIFE: Immediately Invoked Function Expression creating a closure.
Public Interface: Returned object exposing public methods.
Private Members: Variables/functions inside the closure, inaccessible from outside.
Real World Context
While ES modules are preferred today, the Module pattern still appears in legacy code, browser scripts without module bundlers, and understanding closures.
Deep Dive
Classic Module Pattern
The IIFE creates a closure that keeps count and validateCount private. Only the returned object is accessible from outside:
javascriptconst CounterModule = (function() { // Private members let count = 0; function validateCount(n) { return typeof n === 'number' && n >= 0; } // Public interface return { increment() { count++; return count; }, decrement() { if (count > 0) count--; return count; }, getCount() { return count; }, reset() { count = 0; } }; })(); CounterModule.increment(); // 1 CounterModule.count; // undefined (private!)
Accessing CounterModule.count returns undefined because count is a closure variable, not a property of the returned object. The only way to read it is through getCount().
Revealing Module Pattern
The Revealing Module variation defines all functions inside the closure and then returns an object that maps public names to those private functions:
javascriptconst Calculator = (function() { let result = 0; function add(n) { result += n; return result; } function subtract(n) { result -= n; return result; } function getResult() { return result; } function reset() { result = 0; } // Reveal public pointers to private functions return { add, subtract, getResult, reset }; })();
The returned object has short-hand property names that point to the private functions. This makes the public API explicit and easy to scan at a glance.
ES2025 Import Attributes
Import attributes (ES2025) let you specify the expected module type, preventing security issues where a server might serve unexpected content:
javascript// Import JSON with type assertion (ES2025) import config from './config.json' with { type: 'json' }; import styles from './theme.css' with { type: 'css' }; // Dynamic import with attributes const data = await import('./data.json', { with: { type: 'json' } });
The with { type: 'json' } syntax works with both static and dynamic imports. Import attributes tell the engine how to interpret a module, preventing security issues where a server might serve unexpected content types.
Modern ES Modules
ES modules provide built-in encapsulation. Variables declared at module scope are private by default, and only explicitly exported members are accessible:
javascript// counter.js let count = 0; // Private to module export function increment() { return ++count; } export function getCount() { return count; } // main.js import { increment, getCount } from './counter.js'; increment(); // 1
The count variable is module-scoped and cannot be accessed by importers. This is the modern equivalent of the IIFE module pattern, with static analysis and tree-shaking support.
Common Pitfalls
- Memory leaks: Closures hold references to outer scope.
- Testing difficulties: Can't mock private members.
- No dynamic exports: Can't add to public interface after creation.
Best Practices
- Prefer ES modules: Native support, static analysis.
- Use for browser scripts: When modules aren't available.
- Keep modules focused: Single responsibility.
- Document the public interface: Clear API.
Summary
The Module pattern uses closures to create private state. ES modules are the modern replacement with native import/export. Use the pattern when modules aren't available or for understanding closure-based privacy.
Code Examples
// ES module with import attributes (ES2025)
import config from './config.json' with { type: 'json' };
// Private to module scope
let count = 0;
export function increment() {
return ++count;
}
export function getCount() {
return count;
}
// Usage in another file:
// import { increment, getCount } from './counter.js';
// increment(); // 1
// count; // ReferenceError — private to module