Learning Objectives

  • Understand the module pattern and its benefits
  • Create modules with private and public members
  • Implement the revealing module pattern
  • Build singleton modules

What Is the Module Pattern?

The module pattern uses closures to create encapsulated modules with private and public members. It's one of the most popular design patterns in JavaScript for organizing code.

const myModule = (function() {
    // Private variables and functions
    let privateVar = "I'm private";
    
    function privateFunction() {
        console.log(privateVar);
    }
    
    // Public API
    return {
        publicMethod: function() {
            privateFunction();
        },
        
        publicVar: "I'm public"
    };
})();

myModule.publicMethod(); // "I'm private"
console.log(myModule.publicVar); // "I'm public"
console.log(myModule.privateVar); // undefined

IIFE (Immediately Invoked Function Expression)

The module pattern uses an IIFE to create a closure immediately:

// IIFE syntax
(function() {
    // Code here runs immediately
    console.log("I run right away!");
})();

// With return value
const result = (function() {
    return "Hello!";
})();
console.log(result); // "Hello!"

Basic Module Example

const counterModule = (function() {
    // Private state
    let count = 0;
    
    // Private helper
    function logCount() {
        console.log(`Current count: ${count}`);
    }
    
    // Public API
    return {
        increment: function() {
            count++;
            logCount();
        },
        
        decrement: function() {
            count--;
            logCount();
        },
        
        reset: function() {
            count = 0;
            logCount();
        },
        
        getCount: function() {
            return count;
        }
    };
})();

counterModule.increment(); // "Current count: 1"
counterModule.increment(); // "Current count: 2"
counterModule.decrement(); // "Current count: 1"
console.log(counterModule.getCount()); // 1

Practical Example: API Client Module

const apiClient = (function() {
    // Private configuration
    const config = {
        baseURL: 'https://api.example.com',
        timeout: 5000,
        headers: {
            'Content-Type': 'application/json'
        }
    };
    
    // Private helper functions
    function buildURL(endpoint) {
        return `${config.baseURL}${endpoint}`;
    }
    
    function handleResponse(response) {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    }
    
    function handleError(error) {
        console.error('API Error:', error);
        throw error;
    }
    
    // Public API
    return {
        get: async function(endpoint) {
            try {
                const response = await fetch(buildURL(endpoint), {
                    method: 'GET',
                    headers: config.headers,
                    timeout: config.timeout
                });
                return await handleResponse(response);
            } catch (error) {
                return handleError(error);
            }
        },
        
        post: async function(endpoint, data) {
            try {
                const response = await fetch(buildURL(endpoint), {
                    method: 'POST',
                    headers: config.headers,
                    body: JSON.stringify(data),
                    timeout: config.timeout
                });
                return await handleResponse(response);
            } catch (error) {
                return handleError(error);
            }
        },
        
        setBaseURL: function(url) {
            config.baseURL = url;
        },
        
        setTimeout: function(ms) {
            config.timeout = ms;
        }
    };
})();

// Usage
apiClient.get('/users/1').then(user => console.log(user));

The Revealing Module Pattern

A variation where all functions are defined privately and only references are exposed:

const calculatorModule = (function() {
    // Private variables
    let result = 0;
    
    // Private functions
    function add(x) {
        result += x;
        return this;
    }
    
    function subtract(x) {
        result -= x;
        return this;
    }
    
    function multiply(x) {
        result *= x;
        return this;
    }
    
    function divide(x) {
        if (x !== 0) {
            result /= x;
        }
        return this;
    }
    
    function getResult() {
        return result;
    }
    
    function reset() {
        result = 0;
        return this;
    }
    
    // Reveal public pointers to private functions
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide,
        getResult: getResult,
        reset: reset
    };
})();

calculatorModule.add(10).multiply(2).subtract(5);
console.log(calculatorModule.getResult()); // 15

Module with Configuration

const loggerModule = (function() {
    // Private state
    const config = {
        level: 'info',
        prefix: '[LOG]',
        timestamp: true
    };
    
    const levels = {
        debug: 0,
        info: 1,
        warn: 2,
        error: 3
    };
    
    // Private helper
    function formatMessage(level, message) {
        let formatted = config.prefix;
        
        if (config.timestamp) {
            formatted += ` [${new Date().toISOString()}]`;
        }
        
        formatted += ` [${level.toUpperCase()}] ${message}`;
        return formatted;
    }
    
    function shouldLog(level) {
        return levels[level] >= levels[config.level];
    }
    
    // Public API
    return {
        debug: function(message) {
            if (shouldLog('debug')) {
                console.log(formatMessage('debug', message));
            }
        },
        
        info: function(message) {
            if (shouldLog('info')) {
                console.log(formatMessage('info', message));
            }
        },
        
        warn: function(message) {
            if (shouldLog('warn')) {
                console.warn(formatMessage('warn', message));
            }
        },
        
        error: function(message) {
            if (shouldLog('error')) {
                console.error(formatMessage('error', message));
            }
        },
        
        setLevel: function(level) {
            if (levels.hasOwnProperty(level)) {
                config.level = level;
            }
        },
        
        setPrefix: function(prefix) {
            config.prefix = prefix;
        },
        
        enableTimestamp: function(enable) {
            config.timestamp = enable;
        }
    };
})();

loggerModule.info('Application started');
loggerModule.setLevel('warn');
loggerModule.info('This won\'t show');
loggerModule.warn('This will show');

Singleton Pattern

The module pattern creates a singleton by default - only one instance exists:

const appState = (function() {
    // Private state - shared across entire application
    const state = {
        user: null,
        theme: 'light',
        language: 'en'
    };
    
    const listeners = [];
    
    function notifyListeners() {
        listeners.forEach(listener => listener(state));
    }
    
    return {
        getState: function() {
            return {...state}; // Return copy
        },
        
        setUser: function(user) {
            state.user = user;
            notifyListeners();
        },
        
        setTheme: function(theme) {
            state.theme = theme;
            notifyListeners();
        },
        
        setLanguage: function(lang) {
            state.language = lang;
            notifyListeners();
        },
        
        subscribe: function(callback) {
            listeners.push(callback);
            return () => {
                const index = listeners.indexOf(callback);
                if (index > -1) {
                    listeners.splice(index, 1);
                }
            };
        }
    };
})();

// Subscribe to state changes
const unsubscribe = appState.subscribe((state) => {
    console.log('State changed:', state);
});

appState.setTheme('dark'); // Triggers listener
appState.setUser({ name: 'Alice' }); // Triggers listener

Module with Dependencies

// Pass dependencies to module
const userModule = (function(api, logger) {
    // Private
    let currentUser = null;
    
    async function fetchUser(id) {
        try {
            const user = await api.get(`/users/${id}`);
            logger.info(`Fetched user: ${user.name}`);
            return user;
        } catch (error) {
            logger.error(`Failed to fetch user: ${error.message}`);
            throw error;
        }
    }
    
    // Public
    return {
        login: async function(id) {
            currentUser = await fetchUser(id);
            return currentUser;
        },
        
        logout: function() {
            logger.info(`User ${currentUser.name} logged out`);
            currentUser = null;
        },
        
        getCurrentUser: function() {
            return currentUser;
        }
    };
})(apiClient, loggerModule);

// Usage
userModule.login(123).then(user => {
    console.log('Logged in:', user);
});

Benefits of the Module Pattern

  • Encapsulation: Private variables and functions
  • Namespace: Avoids global scope pollution
  • Organization: Groups related functionality
  • Singleton: Single instance by default
  • Clean API: Clear public interface

Key Takeaways

  • ✅ The module pattern uses IIFE and closures
  • ✅ Creates private and public members
  • ✅ The revealing module pattern exposes only references
  • ✅ Modules are singletons by default
  • ✅ Perfect for organizing code and avoiding global pollution

Next Steps

Now that you understand the module pattern, in the next lesson we'll explore currying and partial application - powerful functional programming techniques enabled by closures.