Implementing the Revealing Module Pattern JS
Implement the revealing module pattern in JavaScript. Covers the pattern structure, advantages over the classic module pattern, private method exposure, named exports, testability improvements, and real-world application examples.
The revealing module pattern improves on the classic module pattern by defining all functions privately, then returning an object literal that maps public names to private implementations. This creates a cleaner separation between interface and implementation.
For the classic module pattern, see JavaScript Module Pattern: Advanced Tutorial.
Classic vs Revealing Module
// CLASSIC MODULE PATTERN
// Mixes private and public declarations
const ClassicModule = (function () {
let _count = 0;
function _validate(n) {
return n >= 0 && n <= 100;
}
return {
// Public methods defined inline in the return object
increment() {
if (_validate(_count + 1)) _count++;
return _count;
},
getCount() {
return _count;
},
};
})();
// REVEALING MODULE PATTERN
// All logic is private, return object just maps names
const RevealingModule = (function () {
let count = 0;
function validate(n) {
return n >= 0 && n <= 100;
}
function increment() {
if (validate(count + 1)) count++;
return count;
}
function getCount() {
return count;
}
function reset() {
count = 0;
return count;
}
// Reveal public API by mapping names to private functions
return {
increment,
getCount,
reset,
// You can alias: count: getCount
};
})();| Aspect | Classic Module | Revealing Module |
|---|---|---|
| Function definition | Mixed (some in return, some private) | All defined privately |
| Public API clarity | Must read entire return block | Single return mapping |
| Rename flexibility | Must refactor return object | Change only the mapping |
| Consistency | Mixed styles | Uniform private style |
| Override risk | Public methods are directly on object | References to private closures |
Real-World: Event Emitter
const EventEmitter = (function () {
const listeners = new Map();
let maxListeners = 10;
function on(event, handler) {
if (!listeners.has(event)) {
listeners.set(event, []);
}
const handlers = listeners.get(event);
if (handlers.length >= maxListeners) {
console.warn(`Max listeners (${maxListeners}) reached for "${event}"`);
}
handlers.push(handler);
return off.bind(null, event, handler);
}
function once(event, handler) {
function wrapper(...args) {
off(event, wrapper);
handler.apply(this, args);
}
wrapper._original = handler;
return on(event, wrapper);
}
function off(event, handler) {
if (!listeners.has(event)) return;
const handlers = listeners.get(event);
const index = handlers.findIndex(
(h) => h === handler || h._original === handler
);
if (index !== -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
listeners.delete(event);
}
}
function emit(event, ...args) {
if (!listeners.has(event)) return false;
const handlers = [...listeners.get(event)];
handlers.forEach((handler) => {
try {
handler(...args);
} catch (error) {
console.error(`Error in "${event}" handler:`, error);
}
});
return true;
}
function listenerCount(event) {
return listeners.has(event) ? listeners.get(event).length : 0;
}
function removeAllListeners(event) {
if (event) {
listeners.delete(event);
} else {
listeners.clear();
}
}
function setMaxListeners(n) {
maxListeners = n;
}
// Reveal only the public API
return {
on,
once,
off,
emit,
listenerCount,
removeAllListeners,
setMaxListeners,
};
})();
EventEmitter.on("data", (payload) => console.log("Received:", payload));
EventEmitter.emit("data", { id: 1 });Real-World: HTTP Client
const HttpClient = (function () {
let baseUrl = "";
let defaultHeaders = { "Content-Type": "application/json" };
const interceptors = { request: [], response: [] };
function configure(options) {
if (options.baseUrl) baseUrl = options.baseUrl;
if (options.headers) {
defaultHeaders = { ...defaultHeaders, ...options.headers };
}
}
function addInterceptor(type, handler) {
if (interceptors[type]) {
interceptors[type].push(handler);
}
}
async function applyRequestInterceptors(config) {
let result = config;
for (const interceptor of interceptors.request) {
result = await interceptor(result);
}
return result;
}
async function applyResponseInterceptors(response) {
let result = response;
for (const interceptor of interceptors.response) {
result = await interceptor(result);
}
return result;
}
async function request(method, endpoint, options = {}) {
let config = {
method,
url: `${baseUrl}${endpoint}`,
headers: { ...defaultHeaders, ...options.headers },
body: options.body ? JSON.stringify(options.body) : undefined,
};
config = await applyRequestInterceptors(config);
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body,
});
let result = {
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers),
data: await response.json().catch(() => null),
};
result = await applyResponseInterceptors(result);
if (!result.ok) {
throw Object.assign(new Error(`HTTP ${result.status}`), result);
}
return result;
}
function get(endpoint, options) {
return request("GET", endpoint, options);
}
function post(endpoint, body, options) {
return request("POST", endpoint, { ...options, body });
}
function put(endpoint, body, options) {
return request("PUT", endpoint, { ...options, body });
}
function del(endpoint, options) {
return request("DELETE", endpoint, options);
}
return {
configure,
addInterceptor,
get,
post,
put,
delete: del,
};
})();
HttpClient.configure({ baseUrl: "https://api.example.com" });
HttpClient.addInterceptor("request", (config) => {
config.headers.Authorization = `Bearer ${getToken()}`;
return config;
});Revealing Module Factory
// Factory version creates independent instances
function createRouter(options = {}) {
const routes = new Map();
const middleware = [];
let currentPath = "";
let notFoundHandler = null;
const basePath = options.basePath || "";
function use(handler) {
middleware.push(handler);
}
function route(path, handler) {
const fullPath = basePath + path;
const paramPattern = fullPath.replace(/:(\w+)/g, "(?<$1>[^/]+)");
routes.set(fullPath, {
regex: new RegExp(`^${paramPattern}$`),
handler,
});
}
function notFound(handler) {
notFoundHandler = handler;
}
async function navigate(path) {
// Run middleware
for (const mw of middleware) {
const result = await mw(path, currentPath);
if (result === false) return;
}
currentPath = path;
history.pushState(null, "", path);
for (const [, routeDef] of routes) {
const match = path.match(routeDef.regex);
if (match) {
routeDef.handler(match.groups || {});
return;
}
}
if (notFoundHandler) notFoundHandler(path);
}
function getCurrentPath() {
return currentPath;
}
function start() {
window.addEventListener("popstate", () => navigate(location.pathname));
navigate(location.pathname);
}
// Reveal public API
return {
use,
route,
notFound,
navigate,
getCurrentPath,
start,
};
}
const mainRouter = createRouter();
const adminRouter = createRouter({ basePath: "/admin" });Conditional Exposure Pattern
// Expose different APIs based on environment
const AppConfig = (function () {
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
debug: false,
};
const secretKeys = {
apiKey: "sk_live_abc123",
encryptionKey: "enc_xyz789",
};
function get(key) {
return config[key];
}
function set(key, value) {
if (key in config) {
config[key] = value;
}
}
function getAll() {
return { ...config };
}
function getSecret(key) {
return secretKeys[key];
}
function toJSON() {
return JSON.stringify(config, null, 2);
}
// Standard public API
const publicAPI = { get, set, getAll, toJSON };
// Extended API for development/testing
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
publicAPI.getSecret = getSecret;
publicAPI._config = config;
publicAPI._reset = () => Object.assign(config, { debug: false, retries: 3 });
}
return publicAPI;
})();Rune AI
Key Insights
- All functions are defined privately then revealed: The return object maps public names to private closures, making the API declaration a single readable block
- Factory functions create independent instances: Use factory versions when multiple instances are needed, reserving IIFEs for singleton modules
- Private names are minification-safe: Local variable names inside the IIFE can be safely mangled by minifiers without affecting the public interface
- The return object is a clean API manifest: Scanning the return statement shows every public method and property at a glance
- Conditional exposure adapts the API per environment: Return different property sets based on environment flags to expose debug tools only in development
Frequently Asked Questions
What is the main advantage of the revealing module over the classic module?
Can I override a public method in the revealing module pattern?
How does the revealing module pattern affect minification?
When should I use a factory function instead of an IIFE?
Is the revealing module pattern still relevant with ES modules?
Conclusion
The revealing module pattern improves readability by separating private implementation from public API mapping. All functions are defined privately, and the return object serves as a clear API manifest. For the classic module foundations, see JavaScript Module Pattern: Advanced Tutorial. For encapsulation with singleton guarantees, see JavaScript Singleton Pattern: Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.