How to Design a Real-Time Chat System with MongoDB and Express.js (2025 Guide)

Author

Kritim Yantra

Aug 03, 2025

How to Design a Real-Time Chat System with MongoDB and Express.js (2025 Guide)

📱 “Can you build a chat app?”

That’s a question you’ve probably heard—or Googled—if you’re diving into full-stack development.

Here’s the catch: Chat apps look easy on the outside, but under the hood, they need to be real-time, secure, scalable, and efficiently structured in the database.
Whether you're building a Slack-style team chat, a social DMs feature, or a customer support inbox, you’ll need a solid plan.

In this post, you’ll learn how to design a real-time chat system using:

MongoDB for storing messages and user metadata
Express.js for handling the API
Socket.IO for real-time messaging

We’ll walk through schema design, API structure, real-time event handling, and basic authentication—step by step.

️ This is NOT just theory. Everything here is designed to help you ship faster and avoid common architecture mistakes.


🧱 Key Features of Our Chat System

  • 🔒 User Authentication
  • 👥 1-to-1 and Group Chats
  • 💬 Message History
  • 🟢 Online/Offline Status
  • 📡 Real-Time Messaging with Socket.IO
  • 📦 Efficient MongoDB Schema Design

🗃️ Step 1: Database Schema Design (MongoDB)

🧍 Users Collection

{
  _id: ObjectId,
  username: "john_doe",
  email: "john@example.com",
  password: "<hashed>",
  avatarUrl: "/img/1.jpg",
  status: "online" | "offline",
  lastSeen: ISODate
}

💬 Messages Collection

{
  _id: ObjectId,
  senderId: ObjectId,
  chatId: ObjectId,
  text: "Hey, how are you?",
  readBy: [ObjectId], // users who have read this message
  createdAt: ISODate
}

👥 Chats Collection

{
  _id: ObjectId,
  name: "Team Alpha", // for group chats
  isGroup: true,
  participants: [ObjectId], // user IDs
  lastMessage: ObjectId, // reference to last message
  createdAt: ISODate
}

💡 Pro Tip: Store lastMessage inside the chat document for performance, especially for chat previews.


️ Step 2: Setting Up Express.js API Routes

🧑💻 User Routes

Method Route Description
POST /api/register Register a new user
POST /api/login Authenticate + JWT
GET /api/users List users

📬 Chat Routes

Method Route Description
POST /api/chats Create 1-on-1 or group chat
GET /api/chats/:userId Get user’s chats
POST /api/chats/:id/message Send a message to a chat
GET /api/chats/:id Get message history

📡 Step 3: Adding Real-Time Messaging with Socket.IO

🔌 Setup Server with Socket.IO

const http = require('http');
const socketIO = require('socket.io');
const server = http.createServer(app);
const io = socketIO(server, {
  cors: { origin: "*" }
});

👂 Listen for Events

io.on('connection', socket => {
  console.log("User connected:", socket.id);

  socket.on('joinChat', (chatId) => {
    socket.join(chatId);
  });

  socket.on('sendMessage', async ({ chatId, senderId, text }) => {
    const message = await Message.create({ chatId, senderId, text });
    io.to(chatId).emit('receiveMessage', message);
  });

  socket.on('disconnect', () => {
    console.log("User disconnected");
  });
});

🔄 Clients join chat rooms using join(chatId) so messages broadcast only to relevant users.


🛡️ Step 4: Authentication with JWT

🔐 Register & Login (Express)

const jwt = require('jsonwebtoken');

const login = async (req, res) => {
  const user = await User.findOne({ email: req.body.email });
  const isMatch = await bcrypt.compare(req.body.password, user.password);

  if (!isMatch) return res.status(401).send("Invalid credentials");

  const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1d" });
  res.json({ token });
};

🔐 Middleware to Protect Routes

const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).send("No token");

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch {
    res.status(401).send("Invalid token");
  }
};

📊 Bonus: Track Online/Offline Users

🔴 Use Socket.IO for Presence

const onlineUsers = new Map();

io.on('connection', socket => {
  const userId = socket.handshake.query.userId;
  onlineUsers.set(userId, socket.id);

  socket.on('disconnect', () => {
    onlineUsers.delete(userId);
  });
});

🟢 Broadcast Online Users

io.emit('onlineUsers', Array.from(onlineUsers.keys()));

📌 You can use this to show green dots beside user avatars, or trigger "user is typing" indicators.


🔁 Optional: Typing Indicators, Read Receipts

✍️ Typing Status

socket.on('typing', ({ chatId, userId }) => {
  socket.to(chatId).emit('typing', userId);
});

✅ Mark as Read

socket.on('markAsRead', async ({ messageId, userId }) => {
  await Message.updateOne({ _id: messageId }, { $addToSet: { readBy: userId } });
});

🔚 Conclusion

Designing a chat system isn’t just about throwing messages into a database. You need clear structure, secure auth, and real-time interactions that scale.

Let’s recap:

✅ MongoDB handles flexible user/message data
✅ Express.js provides a clean API
✅ Socket.IO keeps everything real-time
✅ JWT keeps users secure
✅ The schema supports 1-on-1, groups, history, and presence

🎯 Next Step: Try building this system with basic UI using React or Vue, and hook into Socket.IO for real-time messages.


🙋️ FAQ

Q1: Should I use WebSockets or Socket.IO?

A: Socket.IO is built on top of WebSockets and adds features like reconnection, rooms, and fallback—perfect for beginners.


Q2: Where should I store chat messages?

A: Use MongoDB. For massive scale, archive old messages to a cold-storage collection.


Q3: How do I deploy this in production?

A: Use NGINX + PM2 for Express, and Redis Adapter for scaling Socket.IO across multiple instances.


What would you build with this?

Would you add voice messages? File sharing? Emoji reactions?
Drop your ideas in the comments—let’s brainstorm the perfect chat experience. 👇

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts