Implementing the Revealing Module Pattern JS
The revealing module pattern organizes JavaScript code into clean, encapsulated modules with explicit public APIs. Learn how it works, why it matters, and how to use it today.
The revealing module pattern is a way to structure JavaScript code so that internal details stay hidden and only a chosen public API is exposed. It uses closures to create private scope and a return statement to "reveal" which functions and variables are public.
The name describes exactly what happens: you define everything inside a function, then reveal only the parts you want the outside world to use.
The Problem: No Built-in Privacy
JavaScript did not always have private class fields or module-level scope. Before ES6 modules, everything was global unless you wrapped it in a function:
// Without a module pattern -- everything is global
let count = 0;
function increment() {
count += 1;
updateDisplay();
}
function updateDisplay() {
console.log(count);
}
// Any code can do this:
count = 999; // Oops -- direct mutation of internal stateThe revealing module pattern fixes this by wrapping state and functions inside an IIFE so count and updateDisplay are not reachable from outside.
The Pattern Step by Step
Step 1: Wrap in an IIFE
Wrap your code in an immediately invoked function expression. This creates a private scope:
const Counter = (function () {
// Everything here is private
let count = 0;
function increment() {
count += 1;
}
function getCount() {
return count;
}
// Step 2: reveal the public API
return {
increment,
getCount
};
})();
console.log(Counter.getCount()); // 0
Counter.increment();
console.log(Counter.getCount()); // 1
console.log(Counter.count); // undefined -- privateThe critical line is the return statement. It is a map that says: "the outside world can call increment and getCount, but count stays private." This is where the "revealing" name comes from.
Everything lives in the private scope. The return statement creates pointers from public names to private implementations. External code interacts only with the public API. count is unreachable.
Step 2: Name Internal Functions Clearly
The pattern is easiest to read when internal function names match their public names:
const UserStore = (function () {
const users = [];
function addUser(user) {
users.push(user);
}
function getUsers() {
return [...users]; // Return a copy so callers cannot mutate internal array
}
function removeUser(id) {
const index = users.findIndex(u => u.id === id);
if (index !== -1) users.splice(index, 1);
}
return {
addUser,
getUsers,
removeUser
};
})();Notice the return statement is just a clean list of names. No function definitions. No logic. The return says "these are public" and nothing else.
Step 3: Keep the Return Statement Clean
A good revealing module has a dead-simple return statement:
return {
addUser,
getUsers,
removeUser
};A bad revealing module has logic in the return statement:
// Avoid this
return {
addUser: function(user) { /* ... */ },
getUsers: function() { /* ... */ }
};The first version is the whole point of the pattern. Define everything at the top of the IIFE. Reveal at the bottom. This makes it easy to scan the return statement and know exactly what the module exposes.
The Pattern with ES Modules
If you are using ES modules, you do not need an IIFE. The module file itself is the private boundary:
// counter.js
let count = 0; // Private -- not exported
export function increment() {
count += 1;
}
export function getCount() {
return count;
}Everything not exported is private. The export keyword replaces the return statement as the "reveal" mechanism:
// userStore.js
const users = []; // Private
export function addUser(user) {
users.push(user);
}
export function getUsers() {
return [...users];
}
export function removeUser(id) {
const index = users.findIndex(u => u.id === id);
if (index !== -1) users.splice(index, 1);
}The principle is identical: define everything at module scope, export only the public API. The difference is that import and export replace the IIFE wrapper and the return statement.
ES modules are the modern way to do this. Learn more about how they work in the ES modules import export guide.
Practical Use: Form Validator Module
Here is a realistic module built with the pattern:
const FormValidator = (function () {
const rules = {
required(value) {
return value !== undefined && value !== null && value !== "";
},
email(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
},
minLength(min) {
return (value) => value.length >= min;
}
};
function validateField(value, ruleList) {
const errors = [];
ruleList.forEach(rule => {
if (typeof rule === "string") {
const validatorFn = rules[rule];
if (validatorFn && !validatorFn(value)) {
errors.push(rule);
}
} else if (typeof rule === "object") {
const [name, param] = Object.entries(rule)[0];
const validatorFn = rules[name];
if (validatorFn && !validatorFn(param)(value)) {
errors.push(name);
}
}
});
return errors;
}
function validate(formData, schema) {
const result = { valid: true, errors: {} };
Object.keys(schema).forEach(field => {
const fieldErrors = validateField(formData[field], schema[field]);
if (fieldErrors.length > 0) {
result.valid = false;
result.errors[field] = fieldErrors;
}
});
return result;
}
function addRule(name, fn) {
rules[name] = fn;
}
return {
validate,
addRule
};
})();Usage:
const result = FormValidator.validate(
{ username: "", email: "bad" },
{
username: ["required"],
email: ["required", "email"]
}
);
console.log(result);
// { valid: false, errors: { username: ["required"], email: ["email"] } }The rules object and the validateField helper are private. Only validate and addRule are exposed. Callers get a clean two-method API with all the complexity hidden inside.
Common Mistake: Exposing Mutable References
When you return a reference to a private object or array, callers can mutate it:
const Store = (function () {
const items = ["a", "b"];
return {
getItems() {
return items; // Wrong -- returns the actual array
}
};
})();
const data = Store.getItems();
data.push("malicious");
console.log(Store.getItems()); // ["a", "b", "malicious"] -- internal state mutatedAlways return a copy for arrays and objects:
getItems() {
return [...items];
}When to Use the Revealing Module Pattern
Use it when:
- You need a singleton with private state and a clean public API.
- You are writing a library and want to hide internal helpers from consumers.
- You are organizing code in a script file that does not use a build system.
Skip it when:
- You are already using ES modules with
import/export. The module file itself provides the same boundary. - You need multiple instances of the module. Use a factory function instead.
- You are working in a framework that has its own module or component system.
Rune AI
Key Insights
- The revealing module pattern uses closures to create private state.
- All logic is defined first, then only the public members are returned.
- The return statement acts as a map of public names to private implementations.
- With ES modules, the pattern is simpler: just do not export what should be private.
- It is the foundation for understanding encapsulation in larger JavaScript architectures.
Frequently Asked Questions
Is the revealing module pattern still relevant with ES modules?
What is the difference between the module pattern and the revealing module pattern?
Does the revealing module pattern work without IIFEs?
Conclusion
The revealing module pattern is a clean way to organize code with a clear public API and truly private internals. Its core idea, define everything first, then reveal only what callers need, applies whether you use IIFEs or ES modules.The revealing module pattern is the simplest way to achieve encapsulation in JavaScript. Wrap your logic in a function (or a module file), define everything in private scope, and return (or export) only what callers should use. The return statement is the contract between your module and the outside world. Keep it clean. Keep it short. Everything else stays private. This pattern is the foundation that modern module systems are built on.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.