The JavaScript Module Pattern
Before ES6 brought native modules to JavaScript, developers needed a way to encapsulate code, hide private data, and expose only a clean public API. The Module Pattern solved all three problems using closures and immediately invoked function expressions (IIFEs). Even in a world of import and export, understanding this pattern deepens your knowledge of closures and encapsulation.
The Problem It Solves
Without encapsulation, all your variables and functions live in the global scope, where they can accidentally collide with third-party libraries or other parts of your codebase. The Module Pattern gives you:
- Private state — internal variables that can't be accessed from outside.
- A public API — a controlled surface of functions and values you choose to expose.
- Namespace protection — everything lives inside one object, not polluting the global scope.
Basic Structure
The pattern wraps your code in an IIFE and returns an object containing the public-facing methods:
const Counter = (() => {
// Private state — not accessible from outside
let count = 0;
// Private helper
function validate(value) {
return typeof value === "number";
}
// Public API
return {
increment() {
count++;
},
decrement() {
count--;
},
add(value) {
if (validate(value)) count += value;
},
getCount() {
return count;
},
};
})();
Counter.increment();
Counter.increment();
Counter.add(5);
console.log(Counter.getCount()); // 7
console.log(Counter.count); // undefined — count is private!
Revealing Module Pattern
A popular variation is the Revealing Module Pattern. Instead of defining methods directly on the returned object, you define everything privately and then reveal only what you want:
const ShoppingCart = (() => {
let items = [];
function addItem(item) {
items.push(item);
}
function removeItem(name) {
items = items.filter(i => i.name !== name);
}
function getTotal() {
return items.reduce((sum, i) => sum + i.price, 0);
}
function getItems() {
return [...items]; // Return a copy to protect internal state
}
// Reveal only these methods
return { addItem, removeItem, getTotal, getItems };
})();
ShoppingCart.addItem({ name: "Book", price: 12.99 });
ShoppingCart.addItem({ name: "Pen", price: 1.50 });
console.log(ShoppingCart.getTotal()); // 14.49
The key advantage here is readability — you can scan the return statement at the bottom to instantly see what's public.
Module Pattern vs ES6 Modules
| Feature | Module Pattern (IIFE) | ES6 Modules |
|---|---|---|
| Private state | Via closures | Via unexported variables |
| Public API | Returned object | export keyword |
| File separation | Single file/scope | One module per file |
| Browser support | All browsers | Modern browsers + bundlers |
| Static analysis | Limited | Excellent (tree-shaking) |
When to Use the Module Pattern Today
Modern projects typically use ES6 modules. However, the Module Pattern is still valuable when:
- You're writing a self-contained script without a build step.
- You're maintaining legacy code that predates ES6 modules.
- You need to understand closures at a deep level — this pattern is a perfect teaching tool.
Key Takeaways
The Module Pattern remains one of the most important patterns in JavaScript history. It demonstrates the power of closures to create stateful, encapsulated objects — a concept that underpins everything from React hooks to class-based OOP. Study it, understand it, and you'll write better JavaScript regardless of which modern tool you reach for next.