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.

JavaScriptadvanced
15 min read

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

javascriptjavascript
// 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
  };
})();
AspectClassic ModuleRevealing Module
Function definitionMixed (some in return, some private)All defined privately
Public API clarityMust read entire return blockSingle return mapping
Rename flexibilityMust refactor return objectChange only the mapping
ConsistencyMixed stylesUniform private style
Override riskPublic methods are directly on objectReferences to private closures

Real-World: Event Emitter

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
// 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

javascriptjavascript
// 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

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
RunePowered by Rune AI

Frequently Asked Questions

What is the main advantage of the revealing module over the classic module?

The revealing module defines all functions in the same private scope, then maps them to public names in a single return statement. This makes the public API immediately visible at a glance. In the classic module, public methods are defined inline in the return object, mixing API definition with implementation, making it harder to see the full public interface.

Can I override a public method in the revealing module pattern?

You can reassign a property on the returned object, but the original private function remains unchanged. Since the return object holds references to closures, replacing `module.method` does not affect internal calls to the private function. This is both a safety feature and a limitation compared to the classic module where methods are defined directly on the object.

How does the revealing module pattern affect minification?

Private function names can be mangled by minifiers since they are local variables. Public names in the return object cannot be mangled (they are property names). This means the revealing module minifies well for private code but preserves public API names, which is the desired behavior for any public interface.

When should I use a factory function instead of an IIFE?

Use a factory function when you need multiple independent instances of the module (like multiple router instances or store instances). Use an IIFE when you need exactly one instance (singleton behavior), such as a configuration manager or a global event bus. The factory pattern is also more testable since you can create fresh instances per test.

Is the revealing module pattern still relevant with ES modules?

It is less common in new projects since ES modules provide native encapsulation. However, the pattern is still useful for understanding closure-based privacy, for inline scripts without build tools, and for maintaining legacy codebases. The concepts of public API mapping and private implementation transfer directly to how you structure ES module exports.

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.