Learning Objectives

  • Understand the Module pattern concept
  • Create private and public members
  • Use IIFE for encapsulation
  • Implement revealing module pattern
  • Apply module best practices

What is the Module Pattern?

Module pattern uses IIFE (Immediately Invoked Function Expression) and closures to create private scope and expose only what's needed. It was the primary way to achieve encapsulation before ES6 modules.

Basic Implementation

const Counter = (function() {
    // Private variable - not accessible outside
    let count = 0;
    
    // Private function
    function log() {
        console.log(`Current count: ${count}`);
    }
    
    // Public API - returned object
    return {
        increment() {
            count++;
            log();
            return this;
        },
        
        decrement() {
            count--;
            log();
            return this;
        },
        
        getCount() {
            return count;
        },
        
        reset() {
            count = 0;
            log();
        }
    };
})();

// Usage
Counter.increment(); // Current count: 1
Counter.increment(); // Current count: 2
Counter.decrement(); // Current count: 1

console.log(Counter.getCount()); // 1
console.log(Counter.count); // undefined (private!)

// Method chaining
Counter.increment().increment().increment();

Revealing Module Pattern

A cleaner variation that defines all functions first, then reveals them:

const Calculator = (function() {
    let result = 0;
    
    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) {
            throw new Error('Cannot divide by zero');
        }
        result /= x;
        return this;
    }
    
    function getResult() {
        return result;
    }
    
    function reset() {
        result = 0;
        return this;
    }
    
    // Reveal only what's needed
    return {
        add,
        subtract,
        multiply,
        divide,
        getResult,
        reset
    };
})();

// Usage
Calculator
    .add(10)
    .multiply(2)
    .subtract(5)
    .divide(3);

console.log(Calculator.getResult()); // 5
Calculator.reset();

Real-World Examples

User Manager Module

const UserManager = (function() {
    const users = [];
    let nextId = 1;
    
    function validateEmail(email) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
    
    function findById(id) {
        return users.find(user => user.id === id);
    }
    
    function addUser(name, email) {
        if (!validateEmail(email)) {
            throw new Error('Invalid email');
        }
        
        const user = {
            id: nextId++,
            name,
            email,
            createdAt: new Date()
        };
        
        users.push(user);
        return user;
    }
    
    function removeUser(id) {
        const index = users.findIndex(user => user.id === id);
        if (index === -1) {
            throw new Error('User not found');
        }
        return users.splice(index, 1)[0];
    }
    
    function getUser(id) {
        const user = findById(id);
        if (!user) {
            throw new Error('User not found');
        }
        return { ...user }; // Return copy
    }
    
    function getAllUsers() {
        return users.map(user => ({ ...user }));
    }
    
    return {
        addUser,
        removeUser,
        getUser,
        getAllUsers
    };
})();

// Usage
const user1 = UserManager.addUser('John', 'john@example.com');
const user2 = UserManager.addUser('Jane', 'jane@example.com');

console.log(UserManager.getAllUsers());
// [{ id: 1, name: 'John', ... }, { id: 2, name: 'Jane', ... }]

console.log(UserManager.users); // undefined (private!)

API Client Module

const ApiClient = (function() {
    const baseUrl = 'https://api.example.com';
    let authToken = null;
    
    async function request(endpoint, options = {}) {
        const url = `${baseUrl}${endpoint}`;
        const headers = {
            'Content-Type': 'application/json',
            ...options.headers
        };
        
        if (authToken) {
            headers['Authorization'] = `Bearer ${authToken}`;
        }
        
        const response = await fetch(url, {
            ...options,
            headers
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return response.json();
    }
    
    function setAuthToken(token) {
        authToken = token;
    }
    
    function clearAuthToken() {
        authToken = null;
    }
    
    async function get(endpoint) {
        return request(endpoint, { method: 'GET' });
    }
    
    async function post(endpoint, data) {
        return request(endpoint, {
            method: 'POST',
            body: JSON.stringify(data)
        });
    }
    
    async function put(endpoint, data) {
        return request(endpoint, {
            method: 'PUT',
            body: JSON.stringify(data)
        });
    }
    
    async function del(endpoint) {
        return request(endpoint, { method: 'DELETE' });
    }
    
    return {
        setAuthToken,
        clearAuthToken,
        get,
        post,
        put,
        delete: del
    };
})();

// Usage
ApiClient.setAuthToken('my-token-123');
const users = await ApiClient.get('/users');
const newUser = await ApiClient.post('/users', { name: 'John' });

When to Use Module Pattern

Good use cases:
  • Encapsulation and privacy
  • Private state management
  • Namespace management
  • Legacy code (pre-ES6)
  • Single instance utilities

Benefits

Module Pattern vs ES6 Modules

// Module Pattern (old way)
const MyModule = (function() {
    const private = 'private';
    
    return {
        public() {
            return private;
        }
    };
})();

// ES6 Modules (modern way)
// mymodule.js
const private = 'private';

export function public() {
    return private;
}

// app.js
import { public } from './mymodule.js';
Modern Recommendation: Use ES6 modules for new code. Module pattern is still useful for:
  • Legacy browser support
  • Single-file scripts
  • Understanding closures
  • Maintaining old code

Best Practices

1. Use revealing module pattern for clarity
// Define all functions first
function method1() {}
function method2() {}

// Then reveal
return { method1, method2 };
2. Return copies of private data
function getUsers() {
    return users.map(u => ({ ...u })); // Copy
}
3. Use descriptive names
const UserManager = (function() {
    // Clear purpose
})();

Key Takeaways