Node.js Design Patterns
π Concept
Design patterns are proven solutions to recurring software design problems. In Node.js, certain patterns are especially important due to its asynchronous, event-driven nature.
Essential Node.js patterns:
| Pattern | Purpose | Example |
|---|---|---|
| Singleton | One instance across the app | Database connection, logger |
| Factory | Create objects without exposing logic | Router creation, plugin loading |
| Observer | React to state changes | EventEmitter, Redis Pub/Sub |
| Strategy | Swap algorithms at runtime | Payment processors, auth strategies |
| Middleware | Chain processing steps | Express middleware, validation pipeline |
| Repository | Abstract data access | Database queries behind an interface |
| Dependency Injection | Pass dependencies, don't import | Testable services, modular code |
| Proxy | Control access or add behavior | Caching proxy, logging proxy |
Module pattern in Node.js: Every file is a module β this is the foundation of encapsulation:
// Only exported functions are public
module.exports = { publicFunction };
// Internal helpers stay private to the module
Dependency Injection: Instead of importing dependencies directly (tight coupling), pass them as parameters (loose coupling):
// β Tight coupling
const db = require('./database');
class UserService { findUser(id) { return db.query(...) } }
// β
Loose coupling (dependency injection)
class UserService {
constructor(db) { this.db = db; }
findUser(id) { return this.db.query(...) }
}
π Real-world analogy: Design patterns are like cooking techniques (sautΓ©ing, braising, grilling). They're not recipes β they're reusable methods that experienced chefs (developers) apply to solve common cooking (coding) challenges. Knowing the right technique for the ingredient (problem) makes you a better chef.
π» Code Example
1// Node.js Design Patterns23// 1. Singleton β Database Connection4class Database {5 constructor() {6 if (Database.instance) return Database.instance;7 this.connection = null;8 Database.instance = this;9 }1011 async connect(uri) {12 if (!this.connection) {13 // this.connection = await mongoose.connect(uri);14 this.connection = { uri, connected: true };15 console.log("Database connected");16 }17 return this.connection;18 }1920 static getInstance() {21 if (!Database.instance) new Database();22 return Database.instance;23 }24}2526// 2. Factory β Service Creator27class ServiceFactory {28 static create(type, config) {29 switch (type) {30 case "email":31 return config.provider === "sendgrid"32 ? new SendGridService(config.apiKey)33 : new SMTPService(config.host, config.port);34 case "storage":35 return config.provider === "s3"36 ? new S3Storage(config.bucket)37 : new LocalStorage(config.path);38 default:39 throw new Error(`Unknown service type: ${type}`);40 }41 }42}4344// 3. Strategy β Payment Processing45class PaymentProcessor {46 constructor(strategy) {47 this.strategy = strategy;48 }4950 setStrategy(strategy) {51 this.strategy = strategy;52 }5354 async processPayment(amount, details) {55 return this.strategy.charge(amount, details);56 }57}5859class StripeStrategy {60 async charge(amount, details) {61 console.log(`Charging $${amount} via Stripe`);62 return { provider: "stripe", transactionId: "txn_123" };63 }64}6566class PayPalStrategy {67 async charge(amount, details) {68 console.log(`Charging $${amount} via PayPal`);69 return { provider: "paypal", transactionId: "pp_456" };70 }71}7273// Usage74const processor = new PaymentProcessor(new StripeStrategy());75// processor.processPayment(99.99, { card: "..." });76// Switch strategy at runtime:77// processor.setStrategy(new PayPalStrategy());7879// 4. Repository β Data Access Abstraction80class UserRepository {81 constructor(db) { this.db = db; }8283 async findById(id) {84 // return this.db.collection("users").findOne({ _id: id });85 return { id, name: "Alice" };86 }8788 async findByEmail(email) {89 // return this.db.collection("users").findOne({ email });90 return null;91 }9293 async create(userData) {94 // const result = await this.db.collection("users").insertOne(userData);95 return { id: Date.now(), ...userData };96 }9798 async update(id, data) {99 // return this.db.collection("users").updateOne({ _id: id }, { $set: data });100 return { id, ...data };101 }102103 async delete(id) {104 // return this.db.collection("users").deleteOne({ _id: id });105 return true;106 }107}108109// 5. Dependency Injection Container110class Container {111 constructor() {112 this.services = new Map();113 this.singletons = new Map();114 }115116 register(name, factory, singleton = false) {117 this.services.set(name, { factory, singleton });118 }119120 resolve(name) {121 const service = this.services.get(name);122 if (!service) throw new Error(`Service "${name}" not registered`);123124 if (service.singleton) {125 if (!this.singletons.has(name)) {126 this.singletons.set(name, service.factory(this));127 }128 return this.singletons.get(name);129 }130131 return service.factory(this);132 }133}134135// Usage136const container = new Container();137container.register("db", () => Database.getInstance(), true);138container.register("userRepo", (c) => new UserRepository(c.resolve("db")), true);139container.register("paymentProcessor", () => new PaymentProcessor(new StripeStrategy()));140141const userRepo = container.resolve("userRepo");142const payments = container.resolve("paymentProcessor");143144// Mock classes for examples145class SendGridService { constructor(key) { this.key = key; } }146class SMTPService { constructor(host, port) { this.host = host; } }147class S3Storage { constructor(bucket) { this.bucket = bucket; } }148class LocalStorage { constructor(path) { this.path = path; } }149150module.exports = { Database, ServiceFactory, PaymentProcessor, UserRepository, Container };
ποΈ Practice Exercise
Exercises:
- Implement the Singleton pattern for a database connection and a logger
- Build a Factory that creates different notification services (email, SMS, push) based on configuration
- Implement the Strategy pattern for a payment system with 3+ payment providers
- Create a Repository layer that abstracts database operations β swap between MongoDB and PostgreSQL
- Build a simple Dependency Injection container that supports registration, singleton, and resolution
- Refactor an Express app to use DI β all services receive dependencies via constructors
β οΈ Common Mistakes
Overusing the Singleton pattern β not everything should be a singleton; it makes testing harder because state is shared
Using design patterns for the sake of using them β patterns solve specific problems; don't add a Factory when a simple function will do
Not using Dependency Injection for services β direct imports create tight coupling and make unit testing impossible without module mocking
Mixing business logic with infrastructure β services should not know about Express, HTTP, or databases directly; use abstractions
Creating god objects/services β one class doing everything violates Single Responsibility; split into focused, composable classes
πΌ Interview Questions
π€ Mock Interview
Mock interview is powered by AI for Node.js Design Patterns. Login to unlock this feature.