El patrón observer en Node.js se implementa con Event Emitter, una abstracción nativa del core que te permite crear programas orientados a eventos: tú emites eventos en una parte del código y otras partes los escuchan para reaccionar. Si trabajas con Node, vas a toparte con este patrón todo el tiempo, así que vale la pena entenderlo a fondo.
Aquí vas a construir un notificador de registro de usuarios donde, cada vez que alguien se registra, dos sistemas reaccionan: uno que envía un correo y otro que actualiza estadísticas. Es el ejemplo perfecto para ver cómo el patrón distribuye lógica sin acoplar componentes.
¿Qué es Event Emitter en Node.js y por qué importa?
Event Emitter es la pieza del core que materializa el patrón observer. Tú defines un emisor que lanza eventos con datos, y cualquier listener suscrito ejecuta su lógica cuando ese evento ocurre. Lo poderoso es que el emisor no sabe ni le importa quién lo escucha.
¿Qué hace Event Emitter? Permite que un objeto emita eventos con nombre y datos asociados, y que otros objetos se suscriban con .on() para reaccionar cuando ese evento se dispara.
En la clase se menciona la documentación oficial del core como referencia obligada [01:05]. Ahí encuentras la API completa de la clase EventEmitter.
¿Cómo crear el notificador base con EventEmitter?
Lo primero es construir una abstracción reutilizable. Creas un archivo notifier.js dentro de una carpeta observer y defines una clase que extienda de EventEmitter [02:10].
js
const EventEmitter = require('node:events');
class UserNotifier extends EventEmitter {}
module.exports = new UserNotifier();
Fíjate en la notación node:events. Esto no es un capricho de estilo, es seguridad. Al prefijar con node: le dices a Node que cargue el módulo nativo y nadie puede sustituirlo con un paquete malicioso publicado en NPM con el mismo nombre. Es tu defensa contra ataques de supply chain [02:30].
¿Por qué usar node: al importar módulos del core? Porque garantiza que se cargue el módulo nativo de Node y bloquea ataques de supply chain donde alguien publica un paquete con el nombre de un módulo del core.
Al extender de EventEmitter, tu clase UserNotifier ya tiene métodos como .emit(), .on() y todo lo necesario sin escribir una sola línea extra.
¿Cómo emitir y escuchar eventos en una arquitectura real?
Ahora viene la parte interesante: separar quién emite de quién escucha.
Emitir el evento desde el registro de usuarios
En user-registration.js importas el notificador y creas la función que simula el registro [03:40]:
js
const notifier = require('./notifier');
function registerUser(user) {
console.log('registering user');
notifier.emit('user-registered', user);
return user;
}
module.exports = { registerUser };
El método .emit() lanza el evento user-registered y adjunta el objeto user como payload. Cualquiera suscrito recibe ese dato.
Suscribir listeners independientes
Dentro de una carpeta listeners creas dos archivos. El primero, email-listener.js, escucha y simula el envío de correo [04:30]:
js
const notifier = require('../notifier');
notifier.on('user-registered', (user) => {
console.log(Sending email to ${user.email});
});
El segundo, stats-listener.js, escucha el mismo evento y registra estadísticas:
js
const notifier = require('../notifier');
notifier.on('user-registered', (user) => {
console.log(Logging stats for ${user.name});
});
Los dos listeners reciben exactamente el mismo objeto user porque ambos están suscritos al mismo evento. Ninguno sabe de la existencia del otro.
¿Cómo conectar todo desde el index.js?
En index.js simplemente requieres los listeners y el registrador [06:15]:
js
require('./listeners/email-listener');
require('./listeners/stats-listener');
const { registerUser } = require('./user-registration');
registerUser({ name: 'usuario1', email: 'u1@test.com' });
registerUser({ name: 'usuario2', email: 'u2@test.com' });
No necesitas asignar los listeners a ninguna variable. La suscripción con .on() se ejecuta al importar el módulo, así que con sólo requerirlos quedan activos.
Al ejecutar node index.js ves la secuencia: imprime registering user, luego Sending email y luego Logging stats, repetido para cada usuario. Los listeners se ejecutan en el orden en que se suscribieron.
¿Cuándo conviene usar el patrón observer en Node.js?
Cuando necesitas que varias partes de tu aplicación reaccionen a un mismo hecho sin acoplarlas entre sí. Notificaciones, métricas, logs, integraciones con servicios externos: todo encaja bien aquí.
La ventaja es escalabilidad. Mañana agregas un tercer listener que envíe el usuario a un CRM y no tocas ni el emisor ni los listeners existentes. Sólo creas el archivo nuevo y lo requieres.
Reto para extender el ejercicio
Para afianzar lo aprendido, agrega un segundo evento al sistema [09:10]:
- Crea una función
createPost que simule la creación de un post por un usuario.
- Emite un evento
post-created con los datos del post.
- Haz que
stats-listener también escuche ese evento y lo loguee.
- Crea un nuevo listener tipo blog-channel que reciba el post y lo procese.
No necesitas implementar la lógica real. El objetivo es practicar emisiones y suscripciones para que el patrón te quede natural.
¿Te animas a compartir tu solución del reto en los comentarios?