Event Bus and Observer Pattern in JavaScript (Node.js)
In modern Node.js development, there is often a need to organize loosely coupled interaction between application components. The Observer pattern and its practical implementation — Event Bus — are ideal for solving this problem. In this article, we will take a detailed look at what an Event Bus is, how it relates to the Observer pattern, and create our own implementation in pure JavaScript for Node.js.
What is the Observer Pattern?
The Observer pattern defines a "one-to-many" relationship between objects. When one object (the subject or publisher) changes its state, all dependent objects (observers or subscribers) are automatically notified. In Node.js, this pattern is built into the core via the EventEmitter class.
Main components of the pattern:
- Subject — the object that generates events.
- Observer — the object that subscribes to events.
- Subscription — the mechanism for registering an observer with the subject.
What is an Event Bus?
An Event Bus is a centralized mediator that implements the Observer pattern. Instead of each component directly subscribing to another component, all communicate through a single event bus. This provides the following advantages:
- Loose coupling of components.
- Ease of adding new subscribers without changing the publisher's code.
- Ability to centrally manage events (logging, filtering, debugging).
Built-in EventEmitter in Node.js
Node.js provides a built-in events module that contains the EventEmitter class. This is a ready-made Event Bus implementation. Let's look at a basic example:
const EventEmitter = require('events');const eventBus = new EventEmitter();
// Subscribe to the 'user:login' eventeventBus.on('user:login', (user) => { console.log(`User ${user.name} logged in`);});
// Subscribe with another handlereventBus.on('user:login', (user) => { console.log(`Sending welcome email for ${user.email}`);});
// Emit the eventeventBus.emit('user:login', { name: 'Ivan', email: 'ivan@example.com' });// Output:// User Ivan logged in// Sending welcome email for ivan@example.comCreating Your Own Event Bus
Sometimes the built-in EventEmitter is not enough — for example, if you need to add support for async handlers, priorities, or strict typing. Let's create our own Event Bus implementation from scratch:
class EventBus { constructor() { this.events = {}; }
// Subscribe to an event on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); // Return an unsubscribe function return () => { this.events[eventName] = this.events[eventName] .filter(cb => cb !== callback); }; }
// Subscribe to a one-time event once(eventName, callback) { const wrapper = (...args) => { callback(...args); this.off(eventName, wrapper); }; this.on(eventName, wrapper); }
// Emit an event emit(eventName, ...args) { const callbacks = this.events[eventName]; if (!callbacks) return false; callbacks.forEach(callback => { try { callback(...args); } catch (error) { console.error(`Error in handler for event ${eventName}:`, error); } }); return true; Also in library
Machine Learning in C# (.NET): Overview of Libraries and Tools
GraphQL in C++: cppgraphqlgen Library for Creating APIs
Nodemon for JavaScript (Node.js): Complete Guide with Examples
Sending SMS in C++: LibCurl API Library and REST Requests