DEV Community

Md
Md

Posted on

📨Building a Scalable Notification System Using RabbitMQ in Node.js — A Real-World Practice for CookSync

“Backend isn't just CRUD anymore.”
When I started working on the backend system for CookSync—a recipe-sharing app with real-time features and collaboration in mind— I realized early on that modern apps need asynchronous communication, not just traditional REST APIs.

So, I challenged myself to implement an event-driven notification system using RabbitMQ 🐇.
Here’s the full story of why, how, and what I learned.

🚩 Why I Needed This
Our CookSync app will include features like:

✅ Users following chefs

✅ New recipes being posted

✅ Comments on recipes

✅ Likes/favorites on recipes

Each of these events deserves a notification—ideally sent via email, and eventually through in-app alerts or push notifications.

Initially, I thought of calling an email service directly from the API. But then I asked myself:

❌ What if the email service goes down?
❌ Should my API wait for an email to be sent before responding?
❌ Can I handle hundreds of such events in real time?

That's when I knew it was time to decouple services using RabbitMQ, a battle-tested message broker used in real-world backend architectures.

🛠️ Step-by-Step Implementation
Here’s how I built it from scratch using Node.js, Express, RabbitMQ, Docker, and Nodemailer.

  1. 🔄 Set Up RabbitMQ with Docker bash
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
Enter fullscreen mode Exit fullscreen mode

This pulls the management UI, so I could visit http://localhost:15672 to see queues and exchanges in action. Default user/pass: guest/guest.

  1. 🏗️ Project Structure I created two services:

API Server (Producer)

Notifier (Consumer)

bash
cooksync/
├── server/             # Express API
│   └── routes/         # POST APIs for events
├── notifier/           # RabbitMQ Consumer + Nodemailer
└── .env
Enter fullscreen mode Exit fullscreen mode
  1. 📬 Producer: Publish Event Messages In the API server, I created test routes:
js
// server/routes/notifications.js

router.post('/new-recipe', async (req, res) => {
  const msg = {
    type: 'notify.new_recipe',
    payload: {
      author: 'Chef Khaled',
      title: 'Spicy Biryani',
      owner: 'kbin3140@gmail.com',
    },
  };
  channel.publish('notification_exchange', msg.type, Buffer.from(JSON.stringify(msg)));
  res.send('New recipe event published');
});
Enter fullscreen mode Exit fullscreen mode

We used a topic exchange to route different event types.

  1. 📥 Consumer: RabbitMQ + Nodemailer In the notifier/ service:
js
// notifier/consumer.js

const amqp = require('amqplib');
const nodemailer = require('nodemailer');

(async () => {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();

  await channel.assertExchange('notification_exchange', 'topic');
  const q = await channel.assertQueue('', { exclusive: true });
  await channel.bindQueue(q.queue, 'notification_exchange', 'notify.*');

  channel.consume(q.queue, async (msg) => {
    const data = JSON.parse(msg.content.toString());
    await sendEmailNotification(data.type, data.payload);
    channel.ack(msg);
  });
})();
Enter fullscreen mode Exit fullscreen mode

And the email sender logic:

js
async function sendEmailNotification(type, data) {
  const transporter = nodemailer.createTransport({
    service: 'gmail',
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS,
    },
  });

  let subject, text;
  switch (type) {
    case 'notify.new_recipe':
      subject = `New Recipe from ${data.author}`;
      text = `Check out the new recipe "${data.title}"`;
      break;
    case 'notify.comment':
      subject = `New Comment on your Recipe`;
      text = `${data.commenter} commented: ${data.comment}`;
      break;
    case 'notify.favorite':
      subject = `Your Recipe was Liked`;
      text = `${data.user} liked your recipe: ${data.recipe}`;
      break;
    default:
      subject = `Notification`;
      text = `You have a new event.`;
  }

  await transporter.sendMail({
    from: `"CookSync" <${process.env.EMAIL_USER}>`,
    to: data.owner,
    subject,
    text,
  });

  console.log(`[Email] Sent: ${subject}`);
}
Enter fullscreen mode Exit fullscreen mode

✅ Each event is now picked up asynchronously and results in a real email sent to the user.

  1. ✅ Testing the Flow I tested the system by hitting:

bash

POST http://localhost:3000/api/notifications/new-recipe
Enter fullscreen mode Exit fullscreen mode

Then confirmed:

RabbitMQ shows the message in the exchange

Consumer picks it up

Email lands in my inbox 🎉

🎯 What I Gained from This
✅ Hands-on experience with RabbitMQ as a message broker

✅ Built a decoupled, scalable architecture

✅ Understood asynchronous flows in backend development

✅ Gained confidence to expand this system for push or in-app notifications later

💡 What's Next?
Integrate this into our full CookSync-backend

Add more event types (e.g., real-time notifications using Socket.IO)

Store notifications in MongoDB for in-app view

Add retry logic and dead-letter queues for production readiness

🧠 Final Thoughts
This was a huge step forward in my journey from traditional backend to real-world scalable systems. If you're building anything beyond CRUD, I strongly recommend learning RabbitMQ or another message broker like Kafka.

Top comments (0)