The Module Pattern in JavaScript
The module pattern is one of the most useful design patterns in JavaScript. It provides a way to encapsulate code, create private state, and expose a clean public API. While ES6 modules have largely replaced the traditional module pattern, understanding it helps you appreciate modern JavaScript and gives you tools for scenarios where you need more control over encapsulation.
What Is the Module Pattern?
The module pattern uses closures to create private state that cannot be accessed from outside the module. It combines IIFEs (Immediately Invoked Function Expressions) with closures to achieve encapsulation. The key idea is simple: wrap your code in a function, create variables and functions inside that function, and return only what you want to be public.
const MyModule = (function() {
// Private variables and functions
let privateCounter = 0;
function privateFunction() {
return "This is private";
}
// Public API
return {
publicMethod: function() {
privateCounter++;
return privateFunction();
},
getCounter: function() {
return privateCounter;
}
};
})();
console.log(MyModule.publicMethod()); // This is private
console.log(MyModule.getCounter()); // 1
// These would be undefined or throw errors:
// console.log(MyModule.privateCounter);
// console.log(MyModule.privateFunction());
IIFE: The Foundation
An IIFE (Immediately Invoked Function Expression) is a function that runs immediately after being defined. It’s the foundation of the module pattern because it creates a new scope — a place where your variables and functions can’t be accessed from outside.
// Basic IIFE
(function() {
const secret = "I'm hidden from the outside";
console.log(secret); // Works inside
})();
// console.log(secret); // ReferenceError: secret is not defined
The key insight is that the IIFE creates a closure. The function inside “remembers” the environment where it was created, even after it finishes executing. This is what allows the returned object to access private variables.
The Revealing Module Pattern
A popular variation is the revealing module pattern, where you define all functions and variables privately, then explicitly expose references to the ones you want to be public. This makes it clear exactly what is part of the public API.
const UserManager = (function() {
// Private state
const users = [];
const maxUsers = 100;
// Private functions
function validateUser(user) {
return user && typeof user.name === "string" && user.name.length > 0;
}
function generateId() {
return Math.random().toString(36).substring(2, 9);
}
// Public API - explicitly revealing what we want to expose
return {
addUser: function(user) {
if (!validateUser(user)) {
throw new Error("Invalid user object");
}
if (users.length >= maxUsers) {
throw new Error("Maximum users reached");
}
const newUser = { ...user, id: generateId() };
users.push(newUser);
return newUser;
},
removeUser: function(id) {
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return false;
}
users.splice(index, 1);
return true;
},
getUser: function(id) {
return users.find(u => u.id === id) || null;
},
getAllUsers: function() {
return [...users]; // Return a copy to prevent mutation
},
getCount: function() {
return users.length;
}
};
})();
// Usage
UserManager.addUser({ name: "Alice" });
UserManager.addUser({ name: "Bob" });
console.log(UserManager.getCount()); // 2
console.log(UserManager.getAllUsers());
// [{ name: "Alice", id: "abc123" }, { name: "Bob", id: "def456" }]
// These are private and can't be accessed:
// console.log(users);
// console.log(validateUser());
Namespacing with Modules
One of the main benefits of the module pattern is creating namespaces to avoid global pollution. Instead of adding dozens of functions to the global scope, you group related functionality under a single object.
// Instead of polluting the global scope:
function formatCurrency() { }
function formatDate() { }
function formatNumber() { }
// You create a namespace:
const Formatter = (function() {
function currency(amount, locale = "en-US", currency = "USD") {
return new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(amount);
}
function date(dateObj, locale = "en-US") {
return new Intl.DateTimeFormat(locale).format(dateObj);
}
function number(num, decimals = 2) {
return num.toFixed(decimals);
}
return { currency, date, number };
})();
console.log(Formatter.currency(1234.56)); // $1,234.56
console.log(Formatter.date(new Date())); // 3/17/2026
console.log(Formatter.number(42.567, 1)); // 42.6
Module Pattern vs ES6 Modules
Modern JavaScript uses ES6 modules (import/export), which offer similar benefits but with better tooling and syntax. Here’s how they compare:
// ES6 Module (modern.js)
const privateVariable = "hidden";
export function publicFunction() {
return privateVariable;
}
export default { publicFunction };
// The module pattern (legacy-module.js)
const LegacyModule = (function() {
const privateVariable = "hidden";
function publicFunction() {
return privateVariable;
}
return { publicFunction };
})();
| Feature | Module Pattern | ES6 Modules |
|---|---|---|
| Syntax | Function-based | import/export keywords |
| Static analysis | Limited | Full (tree shaking) |
| Async loading | Manual | Native support |
| Circular dependencies | Error-prone | Supported |
| Private fields | Convention (#) | Truly private |
When to Use Each
The module pattern is still useful in certain scenarios:
- Single-file encapsulation — When you need to isolate code in a single file without setting up a module bundler
- Legacy browser support — For environments that don’t support ES6 modules
- Dynamic module creation — When you need to create modules conditionally at runtime
- Learning JavaScript — Understanding the module pattern helps you understand how JavaScript scoping works
For new projects, prefer ES6 modules unless you have a specific reason not to.
Summary
The module pattern is a powerful tool for JavaScript developers:
- Uses IIFEs and closures to create private state
- The revealing module pattern makes public APIs explicit
- Namespacing reduces global pollution
- While ES6 modules are now preferred, the module pattern remains useful in specific scenarios
Understanding this pattern gives you insight into JavaScript’s scoping mechanics and helps you write more organized, encapsulated code.
See Also
- Functions and Scope — Understand closures and scope, the foundation of the module pattern
- Modules and Imports — Learn about ES6 modules and modern JavaScript module systems
- JavaScript Modules: ESM — Deep dive into ES modules syntax and usage