JavaScript Design Patterns Every Developer Should Know
Patterns aren't just for enterprise Java. They solve real JS problems.
1. Singleton (One Instance Only)
// Class-based singleton
class Database {
static #instance = null;
constructor(config) {
if (Database.#instance) return Database.#instance;
this.config = config;
this.connection = null;
Database.#instance = this;
return this;
}
async connect() {
if (!this.connection) {
this.connection = await createConnection(this.config);
}
return this.connection;
}
}
// Usage: Always returns the same instance
const db1 = new Database({ host: 'localhost' });
const db2 = new Database({ host: 'localhost' });
console.log(db1 === db2); // true — same instance!
// Module-level singleton (simpler, preferred in Node.js)
// database.js
class Database {
constructor() { /* ... */ }
}
module.exports = new Database(); // Single instance created at import time
2. Observer / Event Emitter (Pub-Sub)
class EventEmitter {
#listeners = {};
on(event, callback) {
(this.#listeners[event] ??= []).push(callback);
return () => this.off(event, callback); // Unsubscribe function
}
off(event, callback) {
const listeners = this.#listeners[event];
if (!listeners) return;
this.#listeners[event] = listeners.filter(cb => cb !== callback);
}
emit(event, ...args) {
const listeners = this.#listeners[event];
if (!listeners) return;
listeners.forEach(callback => callback(...args));
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
// Usage:
const store = new EventEmitter();
const unsub = store.on('state-changed', (newState, oldState) => {
console.log(`State changed: ${oldState} → ${newState}`);
});
store.emit('state-changed', { count: 2 }, { count: 1 }); // Logs the change
unsub(); // Stop listening
store.emit('state-changed', { count: 3 }, { count: 2 }); // No log
3. Factory (Create Objects Without Specifying Classes)
// UI component factory
function createUIComponent(type, props) {
const components = {
button: (props) => `<button class="${props.class || ''}">${props.label}</button>`,
input: (props) => `<input type="${props.type || 'text'}" placeholder="${props.placeholder || ''}" />`,
card: (props) => `
<div class="card">
<h3>${props.title}</h3>
<p>${props.content}</p>
</div>`,
modal: (props) => `
<dialog class="modal" open>
<h2>${props.title}</h2>
<p>${props.content}</p>
<button onclick="this.closest('dialog').close()">Close</button>
</dialog>
`,
};
const factory = components[type];
if (!factory) throw new Error(`Unknown component type: ${type}`);
return factory(props);
}
// Usage:
const btn = createUIComponent('button', { label: 'Click me', class: 'primary' });
const form = createUIComponent('input', { type: 'email', placeholder: 'Email' });
4. Strategy (Swap Algorithms at Runtime)
// Validation strategies
const validationStrategies = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
phone: (value) => /^\+?[\d\s-]{7,15}$/.test(value),
password: (value) => value.length >= 8 && /[A-Z]/.test(value) && /[0-9]/.test(value),
username: (value) => /^[a-zA-Z0-9_]{3,20}$/.test(value),
};
class Validator {
constructor(strategy = 'email') {
this.setStrategy(strategy);
}
setStrategy(type) {
if (!validationStrategies[type]) {
throw new Error(`Unknown strategy: ${type}`);
}
this.strategy = validationStrategies[type];
return this; // Chainable!
}
validate(value) {
const isValid = this.strategy(value);
return { isValid, value };
}
}
// Usage:
const validator = new Validator();
validator.setStrategy('email').validate('alex@example.com'); // { isValid: true }
validator.setStrategy('phone').validate('+1-555-1234567'); // { isValid: true }
validator.setStrategy('password').validate('abc'); // { isValid: false }
5. Decorator (Add Behavior Without Modifying Original)
// Function decorators
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with:`, args);
const result = fn.apply(this, args);
console.log(`${fn.name} returned:`, result);
return result;
};
}
function withRetry(fn, maxRetries = 3) {
return async function(...args) {
for (let i = 0; i <= maxRetries; i++) {
try { return await fn.apply(this, args); } catch {}
}
throw new Error(`Failed after ${maxRetries + 1} attempts`);
};
}
function withCache(fn, ttlMs = 5000) {
const cache = new Map();
return async function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
if (Date.now() - timestamp < ttlMs) return data;
}
const data = await fn.apply(this, args);
cache.set(key, { data, timestamp: Date.now() });
return data;
};
}
// Apply decorators
let fetchData = async (url) => {
const res = await fetch(url);
return res.json();
};
fetchData = withLogging(fetchData);
fetchData = withRetry(fetchData, 3);
fetchData = withCache(fetchData, 10000);
await fetchData('/api/users'); // Logged, retried on failure, cached for 10s
6. Adapter (Make Incompatible Interfaces Work Together)
// Different payment providers have different APIs
class StripePaymentAdapter {
constructor(stripeClient) {
this.stripe = stripeClient;
}
async processPayment(amount, currency, token) {
const result = await this.stripe.charges.create({
amount: amount * 100, // Stripe uses cents
currency,
source: token,
});
return { success: result.paid, transactionId: result.id };
}
}
class PayPalPaymentAdapter {
constructor(paypalClient) {
this.paypal = paypalClient;
}
async processPayment(amount, currency, token) {
const order = await this.paypal.createOrder({
intent: 'CAPTURE',
purchase_units: [{ amount: { currency_code: currency, value: String(amount) } }],
});
// ... capture logic ...
return { success: true, transactionId: order.id };
}
}
// Unified interface
class PaymentProcessor {
constructor(adapter) {
this.adapter = adapter;
}
async pay(amount, currency, token) {
return this.adapter.processPayment(amount, currency, token);
}
}
// Swap adapters without changing application code:
const processor = new PaymentProcessor(new StripePaymentAdapter(stripeClient));
// Later: const processor = new PaymentProcessor(new PayPalPaymentAdapter(paypalClient));
await processor.pay(29.99, 'USD', 'tok_visa');
7. Middleware Chain (Like Express!)
class MiddlewareChain {
#middlewares = [];
use(middleware) {
this.#middlewares.push(middleware);
return this; // Chainable
}
async execute(context) {
let index = -1;
const next = async () => {
index++;
if (index < this.#middlewares.length) {
await this.#middlewares[index](context, next);
}
};
await next();
return context;
}
}
// Usage:
const chain = new MiddlewareChain()
.use(async (ctx, next) => {
console.log('1. Before: Parse request');
ctx.parsed = parseRequest(ctx.raw);
await next();
console.log('1. After: Request parsed');
})
.use(async (ctx, next) => {
console.log('2. Before: Authenticate');
ctx.user = await authenticate(ctx.parsed.token);
await next();
console.log('2. After: Authenticated');
})
.use(async (ctx, next) => {
console.log('3. Handle request');
ctx.response = await handleRequest(ctx.user, ctx.parsed);
await next(); // No more middleware after this
});
const result = await chain.execute({ raw: req });
Which pattern do you use most? Any patterns I missed that are JS-specific?
Follow @armorbreak for more JavaScript content.
Top comments (0)