Short answer: To build a MERN stack real time chat app, create an Express server with Socket.IO, store users and messages in MongoDB with Mongoose, protect the connection with JWT login, and build a React client that sends and receives messages through a socket. The whole app takes about eight steps, and a working version needs roughly 300 lines of code.
This guide walks through each step of building a MERN stack chat application in a clear, step-by-step order, with code you can run, a testing checklist, and a section on what changes as the app grows. It is written for developers who know basic JavaScript and want to build a real-time messaging app using React and Node.js that they can extend.
Starting your career in full-stack development? Check Digittrix Academy's guide to full-stack developer salaries for freshers to understand the skills, job roles, and salary expectations that can help you plan your career!
What Is a MERN Stack Real Time Chat App?
A MERN stack real time chat app is a messaging application built with four tools: MongoDB (database), Express (server framework), React (user interface), and Node.js (runtime). A fifth piece, Socket.IO, handles live message delivery.
“Real-time” means a message appears on the other person’s screen without a page refresh. Normal HTTP requests cannot do this because the browser must request an update from the server for each one. A WebSocket connection stays open, so the server can push a message the moment it arrives. Socket.IO builds on WebSockets and falls back to other methods when a network blocks them.
Typical uses include customer support chat, team messaging, buyer-seller chat on marketplaces, and in-app help desks. Startups often build this feature first because it is easy to demonstrate to investors and users.
What You Need Before You Start a MERN Stack Project for Beginners
- Node.js (a current LTS version) and npm installed
- A MongoDB database, either local or a free MongoDB Atlas cluster
- Basic knowledge of React hooks and async JavaScript
- A code editor and a tool such as Postman for testing routes
How the MERN Stack Real Time Chat App Works: Architecture at a Glance
|
Part |
Tool |
Job |
|
Interface |
React |
Shows chats, sends input, listens for new messages |
|
API server |
Express + Node.js |
Handles login, user data and message history |
|
Live channel |
Socket.IO |
Sends messages between connected users instantly |
|
Database |
MongoDB + Mongoose |
Stores users, rooms and messages |
|
Login |
JSON Web Tokens (JWT) |
Proves who is sending each message |
In a Socket.IO chat application, the flow is simple. A user logs in and receives a token. The React app opens a socket connection and sends the token. The server verifies it, then lets the user join a room. When the user sends a message, the server saves it to MongoDB and broadcasts it to everyone in that room.
Step 1: Set Up the Project Folders
Create two folders, one for each side of the app.
bash
mkdir chat-app && cd chat-app
mkdir server client
cd server && npm init -y
npm install express mongoose socket.io jsonwebtoken bcryptjs cors dotenv
npm install -D nodemon
cd ../client && npm create vite@latest . -- --template react
npm install socket.io-client axios
Keeping the server and client separate makes deployment easier, since each can run on its own host.
Step 2: Create the Express Server and Connect MongoDB
Add a .env file in server with MONGO_URI, JWT_SECRET and PORT. Then create server/index.js:
js
require('dotenv').config();
const express = require('express');
const http = require('http');
const mongoose = require('mongoose');
const cors = require('cors');
const { Server } = require('socket.io');
const app = express();
app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: 'http://localhost:5173' }
});
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
server.listen(process.env.PORT || 5000);
Notice that Socket.IO attaches to the HTTP server, not the Express app. This lets them share one port.
Step 3: Define the Mongoose Models for the MongoDB Chat App Project
You need two collections: users and messages.
js
// models/User.js
const mongoose = require('mongoose');
module.exports = mongoose.model('User', new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true }
}));
// models/Message.js
module.exports = mongoose.model('Message', new mongoose.Schema({
room: { type: String, required: true, index: true },
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
text: { type: String, required: true, maxlength: 2000 }
}, { timestamps: true }));
The index on room keeps history queries fast as the collection grows. The maxlength limit stops oversized messages from reaching the database.
Step 4: Add Registration and Login with JWT
Passwords must never be stored as plain text. Use bcrypt to hash them, and return a signed token upon login.
js
// routes/auth.js
const router = require('express').Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
router.post('/register', async (req, res) => {
const hash = await bcrypt.hash(req.body.password, 10);
const user = await User.create({ username: req.body.username, password: hash });
res.status(201).json({ id: user._id });
});
router.post('/login', async (req, res) => {
const user = await User.findOne({ username: req.body.username });
if (!user || !(await bcrypt.compare(req.body.password, user.password)))
return res.status(401).json({ message: 'Wrong username or password' });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1d' });
res.json({ token, username: user.username });
});
module.exports = router;
Mount it in index.js with app.use('/api/auth', require('./routes/auth')).
Step 5: Build the Socket.IO Chat Application Logic on the Server
This is the core of the app. First, a middleware checks the token before any socket connection is established. Then the server handles joining rooms and sending messages.
js
const jwt = require('jsonwebtoken');
const Message = require('./models/Message');
io.use((socket, next) => {
try {
const payload = jwt.verify(socket.handshake.auth.token, process.env.JWT_SECRET);
socket.userId = payload.id;
next();
} catch {
next(new Error('Not authorized'));
}
});
io.on('connection', (socket) => {
socket.on('join_room', (room) => socket.join(room));
socket.on('send_message', async ({ room, text }) => {
const msg = await Message.create({ room, text, sender: socket.userId });
io.to(room).emit('new_message', {
_id: msg._id, room, text, sender: socket.userId, createdAt: msg.createdAt
});
});
socket.on('typing', (room) => socket.to(room).emit('user_typing', socket.userId));
});
Two points matter here. The sender ID comes from the verified token, not from the client message, so users cannot impersonate someone else. And the server saves the message before sending it, so the history remains complete even if a client disconnects.
Step 6: Add a Route for Message History
When a user opens a room, the app should load older messages.
js
router.get('/messages/:room', async (req, res) => {
const messages = await Message.find({ room: req.params.room })
.sort({ createdAt: -1 }).limit(50).populate('sender', 'username');
res.json(messages.reverse());
});
Protect this route with middleware that verifies the same JWT. Load 50 messages at a time to keep the first screen quick. Add a “load more” button that passes the oldest message date as a cursor.
Step 7: Build the React Chat Client
Create a single socket connection and keep it outside the component so it does not reconnect on every render.
jsx
// src/Chat.jsx
import { useEffect, useState, useRef } from 'react';
import { io } from 'socket.io-client';
import axios from 'axios';
export default function Chat({ token, room }) {
const [messages, setMessages] = useState([]);
const [text, setText] = useState('');
const socketRef = useRef(null);
useEffect(() => {
const socket = io('http://localhost:5000', { auth: { token } });
socketRef.current = socket;
socket.emit('join_room', room);
axios.get(`http://localhost:5000/api/messages/${room}`, {
headers: { Authorization: `Bearer ${token}` }
}).then(res => setMessages(res.data));
socket.on('new_message', (msg) => setMessages(prev => [...prev, msg]));
return () => socket.disconnect();
}, [token, room]);
const send = (e) => {
e.preventDefault();
if (!text.trim()) return;
socketRef.current.emit('send_message', { room, text });
setText('');
};
return (
<div>
{messages.map(m => <p key={m._id}>{m.text}</p>)}
<form onSubmit={send}>
<input value={text} onChange={e => setText(e.target.value)} />
<button>Send</button>
</form>
</div>
);
}
The cleanup function in useEffect closes the socket when the component unmounts. Without it, you will see duplicate messages when navigating between pages.
Step 8: Add Typing Indicators and Online Status
These small features make a chat app feel finished. For typing, emit the typing event from the input’s onChange, and show “Someone is typing” for two seconds after each user_typing event. For online status, keep a Map of connected user IDs on the server, update it in the connection and disconnect handlers, and send the list to clients with a presence_update event.
Throttle the typing event so it fires at most once per second. Sending one on every key press wastes bandwidth.
How to Test Your MERN Stack Real Time Chat App
- Open two browser windows, log in as different users, and join the same room.
- Send a message from window one and check that it appears in window two without a refresh.
- Refresh window two and confirm the history loads from MongoDB.
- Disconnect your network for ten seconds, reconnect, and check that Socket.IO restores the connection.
- Try connecting with a wrong token and confirm the server rejects it.
- Send a 3,000-character message and confirm the length limit blocks it.
Common Errors and Fixes
|
Problem |
Likely cause |
Fix |
|
CORS error in the browser |
Origin missing from server settings |
Add the client URL to both Express cors and the Socket.IO cors option |
|
Messages appear twice |
Listener added on every render |
Register listeners inside useEffect and remove them on cleanup |
|
Socket connects, then drops |
Token expired or invalid |
Check JWT_SECRET matches and refresh the token |
|
History is slow |
No index on room |
Add an index and use limit |
|
Works locally, fails after deploy |
Proxy blocks WebSockets |
Enable WebSocket support on the host or reverse proxy |
Security Checklist for Your MERN Stack Chat Application Before You Go Live
- Hash passwords with bcrypt and never log them.
- Store secrets in environment variables, not in code.
- Verify the token on every socket connection and every protected route.
- Check that a user belongs to a room before letting them join it.
- Limit message length and request rates to reduce spam.
- Escape or sanitise message text before showing it, since React escapes by default but any HTML rendering you add later may not.
- Serve the app over HTTPS and use secure WebSocket connections (wss://).
- Review your approach against the OWASP guidance for web application security.
What Changes When the App Grows
One Node.js server can run a small MERN stack real time chat app, but a second server breaks live delivery. A message sent through server A will not reach users connected to server B. The usual fix is the Socket.IO Redis adapter, which forwards events between servers. Add a load balancer with sticky sessions so each client remains on a single server throughout its connection.
Other upgrades to plan for:
- Read receipts and unread counts stored per user per room
- File and image sharing through cloud storage, saving only the link in MongoDB
- Push notifications for offline users
- Message search using MongoDB text indexes or a dedicated search tool
- Moderation tools such as blocking, reporting and admin deletion
For a product that will face customers or investors, these items affect cost and delivery time, so list them in your scope early.
Looking to turn your coding skills into a career? Check Digittrix Academy's guide to full-stack developer training and placement to understand how practical learning and placement support can help you prepare for job opportunities!
Want to Build a MERN Stack Real Time Chat App With Guidance?
A chat app handles live user data and private conversations, so it is best to build one with feedback from someone who has built one. Digittrix Academy teaches the MERN stack through hands-on training, where you build real features such as JWT login, Socket.IO rooms, message history in MongoDB, typing indicators, and deployment. If you want to build these skills with a mentor, send your questions to tanvi@digittrix.com, and the Digittrix Academy team will share course details and batch information.
About the author
Co-Founder: Harsh Abrol
With over 14 years of Experience in the IT Field, helping companies optimise their products for more Conversions.
Categories
MERN DevelopmentTable of Contents
- What Is a MERN Stack Real Time Chat App?
- Step 1: Set Up the Project Folders
- Step 2: Create the Express Server and Connect MongoDB
- Step 3: Define the Mongoose Models for the MongoDB Chat App Project
- Step 4: Add Registration and Login with JWT
- Step 5: Build the Socket.IO Chat Application Logic on the Server
- Step 6: Add a Route for Message History
- Step 7: Build the React Chat Client
- Common Errors and Fixes
- Want to Build a MERN Stack Real Time Chat App With Guidance?