What are the most common design patterns in Node.js?
Design Patterns in Node.js
Design patterns are reusable solutions to common programming problems. Node.js has several patterns that are especially relevant due to its asynchronous, event-driven nature.
1. Module Pattern
Node.js modules provide natural encapsulation:
// counter.js — encapsulated state
let count = 0;
module.exports = {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};const counter = require('./counter');
counter.increment();
console.log(counter.getCount()); // 12. Singleton Pattern
Because Node.js caches require(), modules are naturally singletons:
// database.js
class Database {
constructor() {
if (Database.instance) return Database.instance;
this.connection = null;
Database.instance = this;
}
async connect(url) {
if (!this.connection) {
this.connection = await createConnection(url);
}
return this.connection;
}
}
module.exports = new Database();// Both return the same instance
const db1 = require('./database');
const db2 = require('./database');
console.log(db1 === db2); // true3. Observer Pattern (EventEmitter)
The backbone of Node.js — used extensively in streams, HTTP, and custom events:
const EventEmitter = require('events');
class OrderService extends EventEmitter {
placeOrder(order) {
// Business logic...
this.emit('orderPlaced', order);
}
}
const orderService = new OrderService();
// Subscribers
orderService.on('orderPlaced', (order) => {
emailService.sendConfirmation(order);
});
orderService.on('orderPlaced', (order) => {
inventoryService.updateStock(order);
});
orderService.placeOrder({ id: 1, item: 'Laptop' });4. Factory Pattern
Creating objects without exposing the creation logic:
class DatabaseFactory {
static create(type) {
switch (type) {
case 'postgres':
return new PostgresAdapter();
case 'mongodb':
return new MongoAdapter();
case 'redis':
return new RedisAdapter();
default:
throw new Error(`Unknown database type: ${type}`);
}
}
}
const db = DatabaseFactory.create(process.env.DB_TYPE);5. Middleware Pattern (Chain of Responsibility)
Core pattern in Express.js and NestJS:
class MiddlewareChain {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
return this;
}
async execute(context) {
let index = 0;
const next = async () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(context, next);
}
};
await next();
}
}
const chain = new MiddlewareChain();
chain.use(async (ctx, next) => {
console.log('Auth check');
ctx.user = { id: 1 };
await next();
});
chain.use(async (ctx, next) => {
console.log(`User: ${ctx.user.id}`);
await next();
});6. Strategy Pattern
Swappable algorithms at runtime:
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
async pay(amount) {
return this.strategy.process(amount);
}
}
const stripeStrategy = {
process: (amount) => stripe.charges.create({ amount })
};
const paypalStrategy = {
process: (amount) => paypal.payment.create({ amount })
};
// Use Stripe
const processor = new PaymentProcessor(stripeStrategy);
await processor.pay(100);7. Repository Pattern
Abstracts data access from business logic:
class UserRepository {
constructor(database) {
this.db = database;
}
async findById(id) {
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
async create(userData) {
return this.db.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[userData.name, userData.email]
);
}
async findByEmail(email) {
return this.db.query('SELECT * FROM users WHERE email = $1', [email]);
}
}Pattern Summary
| Pattern | Use Case in Node.js |
|---|---|
| Module | Encapsulation, code organization |
| Singleton | DB connections, config, caches |
| Observer | Event-driven communication |
| Factory | Object creation abstraction |
| Middleware | Request processing pipelines |
| Strategy | Swappable algorithms |
| Repository | Data access abstraction |
| Decorator | Extending behavior (NestJS) |
| Proxy | Caching, logging, access control |
Tip: Don't force patterns where they don't fit. Node.js's module system and event-driven nature already encourage clean patterns. Use them when they solve real problems, not for theoretical purity.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.