How to implement module pattern in JavaScript
The module pattern uses closures to create private and public members, providing encapsulation and namespace management in JavaScript. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve used module patterns extensively before ES6 modules to organize code in large applications, creating clean APIs with private implementation details for millions of users.
The most maintainable approach uses IIFE (Immediately Invoked Function Expression) or ES6 modules for encapsulation.
Basic Module Pattern
const Calculator = (function() {
// Private variables
let result = 0
// Private function
function log(message) {
console.log(`[Calculator] ${message}`)
}
// Public API
return {
add(x) {
result += x
log(`Added ${x}, result: ${result}`)
return this
},
subtract(x) {
result -= x
log(`Subtracted ${x}, result: ${result}`)
return this
},
getResult() {
return result
},
reset() {
result = 0
log('Reset')
return this
}
}
})()
// Usage
Calculator.add(5).add(3).subtract(2)
console.log(Calculator.getResult()) // 6
// Private members not accessible
console.log(Calculator.result) // undefined
console.log(Calculator.log) // undefined
Revealing Module Pattern
const UserManager = (function() {
// Private state
const users = []
let currentId = 1
// Private functions
function generateId() {
return currentId++
}
function findById(id) {
return users.find(user => user.id === id)
}
// Public functions
function addUser(name, email) {
const user = {
id: generateId(),
name,
email,
createdAt: new Date()
}
users.push(user)
return user
}
function getUser(id) {
return findById(id)
}
function getAllUsers() {
return [...users] // Return copy
}
function deleteUser(id) {
const index = users.findIndex(user => user.id === id)
if (index !== -1) {
users.splice(index, 1)
return true
}
return false
}
// Reveal public API
return {
add: addUser,
get: getUser,
getAll: getAllUsers,
delete: deleteUser
}
})()
// Usage
const user = UserManager.add('John', '[email protected]')
console.log(user) // { id: 1, name: 'John', ... }
console.log(UserManager.getAll()) // [{ id: 1, ... }]
// Private functions not accessible
console.log(UserManager.generateId) // undefined
Module with Configuration
const Logger = (function(config) {
// Private state
const settings = Object.assign({
level: 'info',
prefix: '[LOG]',
timestamp: true
}, config)
// Private functions
function formatMessage(level, message) {
const timestamp = settings.timestamp
? `[${new Date().toISOString()}] `
: ''
return `${timestamp}${settings.prefix} ${level.toUpperCase()}: ${message}`
}
function shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error']
const currentLevel = levels.indexOf(settings.level)
const messageLevel = levels.indexOf(level)
return messageLevel >= currentLevel
}
// Public API
return {
debug(message) {
if (shouldLog('debug')) {
console.log(formatMessage('debug', message))
}
},
info(message) {
if (shouldLog('info')) {
console.log(formatMessage('info', message))
}
},
warn(message) {
if (shouldLog('warn')) {
console.warn(formatMessage('warn', message))
}
},
error(message) {
if (shouldLog('error')) {
console.error(formatMessage('error', message))
}
},
setLevel(level) {
settings.level = level
}
}
})({ level: 'debug', prefix: '[APP]' })
// Usage
Logger.debug('Debug message')
Logger.info('Info message')
Logger.setLevel('error')
Logger.info('This won\'t show') // Below error level
Singleton Module
const Database = (function() {
let instance
function createInstance() {
// Private state
const connections = []
let connectionId = 1
return {
connect(connectionString) {
const connection = {
id: connectionId++,
connectionString,
connected: true
}
connections.push(connection)
return connection
},
disconnect(id) {
const index = connections.findIndex(c => c.id === id)
if (index !== -1) {
connections[index].connected = false
return true
}
return false
},
getConnections() {
return connections.filter(c => c.connected)
}
}
}
return {
getInstance() {
if (!instance) {
instance = createInstance()
}
return instance
}
}
})()
// Usage
const db1 = Database.getInstance()
const db2 = Database.getInstance()
console.log(db1 === db2) // true (singleton)
Namespace Module
const MyApp = MyApp || {}
MyApp.Utils = (function() {
function debounce(func, wait) {
let timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, args), wait)
}
}
function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
return {
debounce,
throttle
}
})()
MyApp.API = (function() {
const baseUrl = 'https://api.example.com'
async function get(endpoint) {
const response = await fetch(`${baseUrl}${endpoint}`)
return response.json()
}
async function post(endpoint, data) {
const response = await fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
return response.json()
}
return {
get,
post
}
})()
// Usage
const debouncedSearch = MyApp.Utils.debounce(() => {
console.log('Searching...')
}, 300)
MyApp.API.get('/users').then(users => console.log(users))
ES6 Module Pattern
// math.js
const PI = 3.14159
function add(a, b) {
return a + b
}
function multiply(a, b) {
return a * b
}
// Private function
function logOperation(operation, result) {
console.log(`${operation}: ${result}`)
}
// Export public API
export {
PI,
add,
multiply
}
// Usage in another file
import { add, multiply, PI } from './math.js'
console.log(add(2, 3)) // 5
console.log(PI) // 3.14159
// logOperation is not accessible
Module with Events
const EventBus = (function() {
const events = {}
function on(event, callback) {
if (!events[event]) {
events[event] = []
}
events[event].push(callback)
}
function off(event, callback) {
if (events[event]) {
events[event] = events[event].filter(cb => cb !== callback)
}
}
function emit(event, data) {
if (events[event]) {
events[event].forEach(callback => callback(data))
}
}
function clear() {
Object.keys(events).forEach(event => {
events[event] = []
})
}
return {
on,
off,
emit,
clear
}
})()
// Usage
EventBus.on('user:login', (user) => {
console.log('User logged in:', user.name)
})
EventBus.emit('user:login', { name: 'John' })
Best Practice Note
This is how we implemented modules in CoreUI before ES6 modules became standard. The module pattern provides encapsulation through closures, creating private and public members that organize code and prevent global namespace pollution. While ES6 modules are now the standard, understanding the classic module pattern helps when working with legacy code or implementing design patterns that require private state and controlled access.
Related Articles
For related patterns, check out how to implement singleton pattern in JavaScript and how to implement factory pattern in JavaScript.



