“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.
- 🔄 Set Up RabbitMQ with Docker bash
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
This pulls the management UI, so I could visit http://localhost:15672 to see queues and exchanges in action. Default user/pass: guest/guest.
- 🏗️ 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
- 📬 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');
});
We used a topic exchange to route different event types.
- 📥 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);
});
})();
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}`);
}
✅ Each event is now picked up asynchronously and results in a real email sent to the user.
- ✅ Testing the Flow I tested the system by hitting:
bash
POST http://localhost:3000/api/notifications/new-recipe
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)