Building a Real-Time Notification System with MERN Stack and Socket.io: A Step-by-Step Guide
Implementing Real-Time Features in Your MERN Stack Application Using WebSockets and JWT Authentication
Introduction
Imagine you're scrolling through your favorite social media app when, suddenly, a notification pops up telling you that your friend just liked your latest post. It’s instant, it’s engaging, and it keeps you hooked. Real-time notifications are everywhere these days, from social media alerts to breaking news updates, and they play a huge role in how we interact with our apps.
But have you ever wondered how these notifications actually work behind the scenes? How do apps manage to send you updates the moment something happens? The magic lies in a combination of technologies that allow real-time communication between the server and your browser.
In this blog, I’ll walk you through the process of building your own real-time notification system using the MERN stack—MongoDB, Express, React, and Node.js. By the end of this guide, you’ll not only have a functional notification system but also a solid understanding of how to implement real-time features in your own applications.
Whether you’re building the next big social media platform or just want to add some flair to your existing app, real-time notifications are a powerful tool to keep your users engaged. Let’s dive in and start building!
Setting Up the MERN Stack
Before we dive into building our real-time notification system, we need to set up the foundational MERN stack application. If you're new to MERN or just need a refresher, I’ve got you covered! In a previous article, I walked through the step-by-step process of setting up a full-stack application with React, Node.js, and Express.
You can follow that guide here to get your application up and running quickly. The accompanying code is available on GitHub, so you can clone the repository and get started right away.
In summary, the article covers:
Initializing a React project with Vite for a faster and more optimized development experience.
Setting up a backend REST server with Node and Express.
Additionally, configuring Styled Components and Twin Macro to leverage Tailwind CSS for styling your application.
In the following sections, where we put the code together for the notification system, I have used the same starter code from the previous article.
However, in this article, we'll be taking it a step further by setting up MongoDB, which wasn’t covered in the previous guide. MongoDB will be our database of choice, allowing us to store and manage notifications efficiently. Below are the steps to set up MongoDB using MongoDB Atlas.
Setting Up MongoDB with MongoDB Atlas
MongoDB Atlas is a cloud-based service that provides a fully managed MongoDB cluster. Here’s how you can set it up:
Step 1: Create a MongoDB Atlas Account
Visit MongoDB Atlas and sign up for a free account.
Once you’ve signed up and logged in, click on the "Start Free" button to create your first cluster.
Step 2: Create a Free Cluster
On the Atlas dashboard, click on the "Build a Cluster" button.
Choose the cloud provider and region for your cluster. For free-tier clusters, you can select the default settings.
Click "Create Cluster". This process may take a few minutes.
Step 3: Whitelist Your IP Address
After your cluster is created, you’ll need to whitelist your IP address to connect to the cluster.
Go to the "Network Access" tab on the left sidebar.
Click on "Add IP Address".
To allow access from your current IP address, click "Add Current IP Address". You can also use "0.0.0.0/0" to allow access from any IP address (not recommended for production).
Click "Confirm" to save your settings.
Step 4: Create a Database User
Go to the "Database Access" tab on the left sidebar.
Click on "Add New Database User".
Choose a username and password for your database user.
Under "Database User Privileges", select "Atlas Admin" for full access.
Click "Add User" to create the user.
Step 5: Get Your Connection String
Return to the "Clusters" view and click on "Connect" for your cluster.
In the popup window, select "Connect Your Application".
Choose "Node.js" as your driver and select the latest version.
You’ll be provided with a connection string. It will look something like this:
mongodb+srv://<username>:<password>@cluster0.mongodb.net/?retryWrites=true&w=majority
Replace
<username>
and<password>
with the username and password you created earlier.Copy this connection string, as you’ll need it to connect your application to MongoDB.
Once you've completed these steps, your MongoDB Atlas cluster will be ready to use with your MERN stack application. We’ll use this connection string in our Node.js backend to connect to the database and store our notifications.
Implementing the backend
In this section, we’ll build out the backend for our real-time notification system using Node.js, Express, MongoDB, and Socket.io. The backend will handle authentication, manage notifications, and establish WebSocket connections for real-time updates.
Let’s go step by step through the code provided to set everything up.
Setting Up the Project Structure
First, let's organize the project. Make sure you have Node.js and npm installed on your machine.
Initialize a new Node.js project:
mkdir notification-system cd notification-system npm init -y
Install the necessary dependencies:
npm install express mongoose body-parser socket.io jsonwebtoken dotenv cors
express
: Web framework for building APIs.mongoose
: ODM for MongoDB to manage database interactions.body-parser
: Middleware to parse incoming request bodies in JSON format.socket.io
: Library for real-time WebSocket connections.jsonwebtoken
: For handling JWT (JSON Web Tokens) authentication.dotenv
: For loading environment variables from a.env
file.cors
: Middleware to enable Cross-Origin Resource Sharing.
Create the following files and directories:
index.js
: The main entry point of your application.middleware.js
: To handle authentication middleware for both HTTP requests and WebSocket connections..env
: To store environment variables like JWT secret and MongoDB URI.
Setting Up JWT Authentication
We'll use JWT (JSON Web Tokens) for authenticating both HTTP requests and WebSocket connections.
Create
middleware.js
for handling JWT authentication:const jwt = require('jsonwebtoken'); // Middleware to authenticate JWT for HTTP requests const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) return res.sendStatus(401); jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); }; // Middleware to authenticate JWT for WebSocket connections const authenticateSocket = (socket, next) => { const token = socket.handshake.auth.token; if (!token) return next(new Error('Authentication error')); jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return next(new Error('Authentication error')); socket.user = user; next(); }); }; module.exports = { authenticateToken, authenticateSocket };
Explanation:
authenticateToken
: This middleware authenticates JWT for standard HTTP requests. It extracts the token from theAuthorization
header, verifies it, and attaches the user information to the request object.authenticateSocket
: Similar to the HTTP middleware, this function authenticates JWTs during the WebSocket handshake. It verifies the token from theauth
object in the socket handshake and attaches the user data to the socket object.
Create a
.env
file to store sensitive information:JWT_SECRET=your_jwt_secret_key MONGO_URI=your_mongo_uri
Replace
your_jwt_secret_key
with a secret key of your choice andyour_mongo_uri
with the connection string from MongoDB Atlas that you set up earlier.
Implementing the Main Server Logic
Next, we’ll set up the Express server, connect to MongoDB, and implement the WebSocket logic.
Create
index.js
and add the following code:const express = require('express'); const mongoose = require('mongoose'); const bodyParser = require('body-parser'); const { Server } = require('socket.io'); const jwt = require('jsonwebtoken'); const { authenticateToken, authenticateSocket } = require('./middleware'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(bodyParser.json()); require('dotenv').config(); // Connect to MongoDB mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); // Create a notification model const notificationSchema = new mongoose.Schema({ userId: String, message: String, timestamp: { type: Date, default: Date.now }, isRead: { type: Boolean, default: false }, type: String, }); const Notification = mongoose.model('Notification', notificationSchema); // Start the Express server const PORT = 4000; const server = app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); // Set up Socket.IO with the existing Express server const io = new Server(server, { cors: { origin: '*', // Allow CORS for all origins }, }); // Use authentication middleware for WebSocket connections io.use(authenticateSocket); // Handle WebSocket connections io.on('connection', (socket) => { console.log('A user connected'); // Extract userId from socket authentication and join a room const userId = socket.user.userId; // Join the user to a room with their userId socket.join(userId); // Handle disconnection socket.on('disconnect', () => { console.log('User disconnected'); }); }); // API endpoint to get notifications app.get('/notifications', authenticateToken, async (req, res) => { try { const notifications = await Notification.find({ userId: req.user.userId }); res.json(notifications); } catch (error) { res.status(500).json({ error: 'Failed to retrieve notifications' }); } }); // API endpoint to create a new notification app.post('/notifications', authenticateToken, async (req, res) => { try { const { userId, message, type } = req.body; const notification = new Notification({ userId, message, type }); await notification.save(); // Emit the notification to the specific user's room io.to(userId).emit('notification', notification); res.status(201).json(notification); } catch (error) { res.status(500).json({ error: 'Failed to create notification' }); } }); // Authentication endpoint to generate JWT app.post('/login', (req, res) => { const { username, password } = req.body; // Replace with your user authentication logic if (username === 'John' && password === 'password') { const user = { userId: 'user1', username }; const accessToken = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' }); res.json({ accessToken }); } else if (username === 'Jane' && password === 'password2') { const user = { userId: 'user2', username }; const accessToken = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' }); res.json({ accessToken }); } else { res.status(401).json({ error: 'Invalid credentials' }); } });
Explanation:
This file serves as the main entry point for your Node.js application. It sets up the Express server, connects to MongoDB, handles WebSocket connections with Socket.IO, and provides the necessary API endpoints to interact with the notification system.
Importing Required Modules:
const express = require('express'); const mongoose = require('mongoose'); const bodyParser = require('body-parser'); const { Server } = require('socket.io'); const jwt = require('jsonwebtoken'); const { authenticateToken, authenticateSocket } = require('./middleware'); const cors = require('cors');
express: The core web framework for building the REST API.
mongoose: An ODM (Object Data Modeling) library for MongoDB and Node.js. It manages the connection to MongoDB and provides a schema-based solution to model your application data.
body-parser: Middleware to parse incoming JSON request bodies, making it easier to work with the data sent from the client.
socket.io: A library for enabling real-time, bidirectional communication between the server and the client over WebSockets.
jsonwebtoken: Used to handle JWT (JSON Web Token) for user authentication.
cors: Middleware to enable Cross-Origin Resource Sharing, allowing the API to be accessible from different origins (domains).
authenticateToken, authenticateSocket: Custom middleware functions for authenticating HTTP requests and WebSocket connections, respectively.
Configuring the Express Application:
const app = express();
app.use(cors());
app.use(bodyParser.json());
require('dotenv').config();
app.use(cors()): This enables CORS for all origins, allowing clients from any domain to make requests to your API. This is particularly important in development when your frontend and backend might run on different ports.
app.use(bodyParser.json()): This middleware automatically parses incoming JSON data and attaches it to the
req.body
object, simplifying the handling of JSON payloads in your routes.require('dotenv').config(): This loads environment variables from a
.env
file intoprocess.env
, allowing you to securely manage sensitive information like your JWT secret and MongoDB URI.
Connecting to MongoDB:
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
- mongoose.connect: This establishes a connection to your MongoDB database using the URI stored in the
.env
file. TheuseNewUrlParser
anduseUnifiedTopology
options ensure that you're using the latest MongoDB driver features and connection handling.
Defining the Notification Schema and Model:
const notificationSchema = new mongoose.Schema({
userId: String,
message: String,
timestamp: { type: Date, default: Date.now },
isRead: { type: Boolean, default: false },
type: String,
});
const Notification = mongoose.model('Notification', notificationSchema);
notificationSchema: This defines the structure of a notification document in MongoDB. It includes fields like
userId
,message
,timestamp
,isRead
, andtype
. Thetimestamp
field automatically defaults to the current date and time.Notification: This is a Mongoose model based on the
notificationSchema
. It represents the collection of notifications in your MongoDB database and provides an interface to interact with these documents (e.g., creating, querying, updating).
Starting the Express Server:
const PORT = 4000;
const server = app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
- app.listen: This starts the Express server on the specified
PORT
(4000 in this case) and begins listening for incoming HTTP requests. The callback function logs a message to the console once the server is running.
Setting Up Socket.IO for Real-Time Communication:
const io = new Server(server, {
cors: {
origin: '*', // Allow CORS for all origins
},
});
// Use authentication middleware for WebSocket connections
io.use(authenticateSocket);
new Server(server, { cors: { origin: '*' }}): This initializes a Socket.IO server, attaching it to the existing Express server. The CORS configuration here allows WebSocket connections from any origin.
io.use(authenticateSocket): This applies the
authenticateSocket
middleware to all WebSocket connections. Before a client can establish a connection, the server verifies the client's JWT to ensure they are authenticated.
Handling WebSocket Connections:
io.on('connection', (socket) => {
console.log('A user connected');
// Extract userId from socket authentication and join a room
const userId = socket.user.userId;
// Join the user to a room with their userId
socket.join(userId);
// Handle disconnection
socket.on('disconnect', () => {
console.log('User disconnected');
});
});
io.on('connection'): This event listener is triggered whenever a new client establishes a WebSocket connection with the server. The connected client is represented by the
socket
object.const userId = socket.user.userId; The
authenticateSocket
middleware attaches the authenticated user data to thesocket
object. Here, theuserId
is extracted from this data.socket.join(userId): This line adds the client to a "room" identified by their
userId
. This enables you to emit notifications specifically to this user.socket.on('disconnect'): This event listener is triggered when a client disconnects. It logs a message to the console to indicate the disconnection.
Defining API Endpoints:
Fetching Notifications:
app.get('/notifications', authenticateToken, async (req, res) => {
try {
const notifications = await Notification.find({ userId: req.user.userId });
res.json(notifications);
} catch (error) {
res.status(500).json({ error: 'Failed to retrieve notifications' });
}
});
GET /notifications: This endpoint retrieves all notifications for the authenticated user. The
authenticateToken
middleware ensures that only authenticated users can access this route.Notification.find({ userId: req.user.userId }): This query fetches all notifications from the database that match the
userId
of the authenticated user.res.json(notifications): The server responds with the list of notifications in JSON format.
Creating a New Notification:
app.post('/notifications', authenticateToken, async (req, res) => {
try {
const { userId, message, type } = req.body;
const notification = new Notification({ userId, message, type });
await notification.save();
// Emit the notification to the specific user's room
io.to(userId).emit('notification', notification);
res.status(201).json(notification);
} catch (error) {
res.status(500).json({ error: 'Failed to create notification' });
}
});
POST /notifications: This endpoint allows an authenticated user to create a new notification.
const notification = new Notification({ userId, message, type }); This creates a new instance of the
Notification
model with the data provided in the request body (userId
,message
,type
).await notification.save(); The notification is saved to the MongoDB database.
io.to(userId).emit('notification', notification); The newly created notification is emitted in real-time to the specific user’s room via WebSocket.
User Authentication Endpoint:
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Replace with your user authentication logic
if (username === 'John' && password === 'password') {
const user = { userId: 'user1', username };
const accessToken = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ accessToken });
} else if (username === 'Jane' && password === 'password2') {
const user = { userId: 'user2', username };
const accessToken = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ accessToken });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
POST /login: This endpoint simulates a basic login process, validating the username and password provided in the request.
jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' }): If the credentials are valid, a JWT is generated with the user’s information (
userId
andusername
) as the payload. The token is signed using the secret key from the.env
file and is set to expire in 1 hour.res.json({ accessToken }); The JWT is returned to the client, who will use it to authenticate future requests and WebSocket connections.
Building the frontend
In this section, we'll build the frontend of our real-time notification system using React. If you haven't set up your React application yet, you can refer to this article, where I provide a detailed guide on how to get started with React, including initial configuration with Vite, Styled Components, and Twin Macro for Tailwind CSS.
Now, let’s walk through the App.jsx
file step by step to understand how it integrates with the backend and handles real-time notifications.
Overview of App.jsx
import styled from 'styled-components';
import tw from 'twin.macro';
import vite from '/vite.svg';
import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
const token = localStorage.getItem('accessToken'); // Retrieve token from storage
const socket = io('http://localhost:4000', {
auth: {
token,
},
});
const App = () => {
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const fetchNotifications = async () => {
const token = localStorage.getItem('accessToken'); // Retrieve token from storage
const response = await fetch('http://localhost:4000/notifications', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
console.log('data', data);
setNotifications(data);
};
// Fetch initial notifications from the server
fetchNotifications();
// Listen for new notifications
socket.on('notification', (notification) => {
setNotifications((prevNotifications) => [
notification,
...prevNotifications,
]);
});
// Cleanup on component unmount
return () => {
socket.off('notification');
};
}, []);
return (
<ApplicationContainer>
<SectionContainer>
<Image src={vite} alt='Vite Logo' />
<CenteredText>
<H2>Demonstration</H2>
<Title>Notification-system</Title>
</CenteredText>
<NotificationSection>
<NotificationHeader>Notifications</NotificationHeader>
<NotificationList>
{notifications.map((notification) => (
<NotificationItem key={notification._id}>
{notification.message}
</NotificationItem>
))}
</NotificationList>
</NotificationSection>
</SectionContainer>
</ApplicationContainer>
);
};
export default App;
const ApplicationContainer = tw.div`bg-gradient-to-r from-indigo-500 to-purple-500 h-screen flex items-center justify-center`;
const SectionContainer = tw.div`bg-white shadow-lg rounded-lg p-8 max-w-3xl mx-auto`;
const CenteredText = tw.div`text-center`;
const H2 = tw.h2`font-bold text-lg text-indigo-600 uppercase tracking-wider mb-2`;
const Title = tw.p`text-gray-800 text-3xl sm:text-4xl font-extrabold leading-tight`;
const Image = tw.img`h-20 w-auto mx-auto my-6`;
const NotificationSection = tw.div`mt-10`;
const NotificationHeader = tw.h3`text-xl font-semibold text-gray-700 mb-4`;
const NotificationList = tw.ul`space-y-3`;
const NotificationItem = styled.li`
${tw`bg-indigo-100 text-indigo-700 p-4 rounded-lg shadow-md transition-all duration-300 ease-in-out`}
&:hover {
${tw`bg-indigo-200`}
}
`;
Step-by-Step Explanation
Importing Required Modules and Assets
import styled from 'styled-components';
import tw from 'twin.macro';
import vite from '/vite.svg';
import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
styled-components & twin.macro: These libraries are used to style React components using a mix of Styled Components and Tailwind CSS.
tw
is a utility from Twin Macro that allows you to write Tailwind CSS classes within Styled Components.vite: The Vite logo is imported as an image asset, which will be displayed in the application.
React: The core React library is imported along with
useEffect
anduseState
hooks to manage component state and side effects.socket.io-client: This library enables WebSocket communication between the frontend and backend.
Setting Up the WebSocket Connection
const token = localStorage.getItem('accessToken'); // Retrieve token from storage
const socket = io('http://localhost:4000', {
auth: {
token,
},
});
token: The JWT token stored in
localStorage
is retrieved to authenticate the WebSocket connection. This token was likely obtained during the user login process.socket: A WebSocket connection is established to the backend server at
http://localhost:4000
. Theauth
object contains thetoken
, which is passed to the backend for authentication during the WebSocket handshake.
Creating the Main Component
const App = () => {
const [notifications, setNotifications] = useState([]);
- useState: Initializes the
notifications
state as an empty array. This state will hold the list of notifications received from the server and will be updated as new notifications are received.
Fetching Initial Notifications
useEffect(() => {
const fetchNotifications = async () => {
const token = localStorage.getItem('accessToken'); // Retrieve token from storage
const response = await fetch('http://localhost:4000/notifications', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
console.log('data', data);
setNotifications(data);
};
// Fetch initial notifications from the server
fetchNotifications();
useEffect: The
useEffect
hook is used to run side effects—in this case, fetching initial notifications from the server when the component mounts.fetchNotifications: This asynchronous function sends a GET request to the
/notifications
endpoint of the backend to retrieve the notifications for the authenticated user. The JWT token is included in theAuthorization
header asBearer <token>
.setNotifications: The retrieved notifications are stored in the
notifications
state, which will be used to render the notifications in the UI.
Listening for New Notifications
// Listen for new notifications
socket.on('notification', (notification) => {
setNotifications((prevNotifications) => [
notification,
...prevNotifications,
]);
});
// Cleanup on component unmount
return () => {
socket.off('notification');
};
}, []);
socket.on('notification'): This listens for
notification
events sent by the backend through the WebSocket connection. When a new notification is received, it is added to the top of thenotifications
state array using thesetNotifications
function.Cleanup: The return statement in
useEffect
ensures that the event listener is removed when the component unmounts, preventing memory leaks.
Rendering the UI
return (
<ApplicationContainer>
<SectionContainer>
<Image src={vite} alt='Vite Logo' />
<CenteredText>
<H2>Demonstration</H2>
<Title>E2E-Notification-system</Title>
</CenteredText>
<NotificationSection>
<NotificationHeader>Notifications</NotificationHeader>
<NotificationList>
{notifications.map((notification) => (
<NotificationItem key={notification._id}>
{notification.message}
</NotificationItem>
))}
</NotificationList>
</NotificationSection>
</SectionContainer>
</ApplicationContainer>
);
ApplicationContainer & SectionContainer: These styled components define the overall layout and appearance of the application.
ApplicationContainer
centers the content on the screen with a gradient background, whileSectionContainer
provides a white, rounded box with shadows for the main content.Image: The Vite logo is displayed at the top of the application.
CenteredText, H2, Title: These components center and style the title and subtitle text.
NotificationSection: This section contains the list of notifications, with each notification being rendered as a
NotificationItem
.notifications.map: The
map
function iterates over thenotifications
array and renders each notification message inside a styled list item (NotificationItem
).
Styled Components
const ApplicationContainer = tw.div`bg-gradient-to-r from-indigo-500 to-purple-500 h-screen flex items-center justify-center`;
const SectionContainer = tw.div`bg-white shadow-lg rounded-lg p-8 max-w-3xl mx-auto`;
const CenteredText = tw.div`text-center`;
const H2 = tw.h2`font-bold text-lg text-indigo-600 uppercase tracking-wider mb-2`;
const Title = tw.p`text-gray-800 text-3xl sm:text-4xl font-extrabold leading-tight`;
const Image = tw.img`h-20 w-auto mx-auto my-6`;
const NotificationSection = tw.div`mt-10`;
const NotificationHeader = tw.h3`text-xl font-semibold text-gray-700 mb-4`;
const NotificationList = tw.ul`space-y-3`;
const NotificationItem = styled.li`
${tw`bg-indigo-100 text-indigo-700 p-4 rounded-lg shadow-md transition-all duration-300 ease-in-out`}
&:hover {
${tw`bg-indigo-200`}
}
`;
ApplicationContainer: This component styles the main container of the application with a gradient background and centers the content using flexbox.
SectionContainer: A white, shadowed box with rounded corners and padding to contain the main content.
CenteredText, H2, Title: Text components styled to be centered and display titles and subtitles with specific fonts, sizes, and colors.
NotificationSection & NotificationList: Layout containers for the notification section and list.
NotificationList
arranges notifications with spacing between them.NotificationItem: Each notification is rendered as a styled list item. The item has a background color, padding, rounded corners, and a hover effect that changes the background color slightly.
Testing the Notification System
Sequence Diagram
Before we begin testing our notification system end-to-end, we should refer to the following sequence diagram for clarity on the sequence of events that will occur during the testing process.
User Action (Postman):
The user enters their credentials (username and password) in Postman.
The user makes a POST request to the
/login
endpoint on the Node.js backend using Postman.
Node.js Backend (Login Process):
The Node.js backend receives the credentials from Postman.
The backend verifies the credentials against its stored user data.
If the credentials are valid, the backend generates a JWT (JSON Web Token) for the user.
The backend returns the JWT token to Postman.
User Action (Storing JWT):
- The user takes the JWT token received in Postman and manually places it in the browser’s local storage.
React Frontend (Using JWT for Requests):
The React frontend retrieves the JWT token from the browser’s local storage.
The React frontend uses the JWT token to make a request to the Node.js backend for notifications.
Node.js Backend (Fetching Notifications):
The Node.js backend receives the request for notifications, including the JWT token for authentication.
The backend verifies the JWT token.
The backend queries MongoDB to fetch notifications associated with the authenticated user.
MongoDB (Querying Notifications):
- MongoDB processes the query and returns the relevant notifications to the Node.js backend.
Node.js Backend (Returning Notifications):
- The Node.js backend sends the retrieved notifications back to the React frontend.
React Frontend (Establishing WebSocket Connection):
- The React frontend establishes a WebSocket connection with the WebSocket server, using the JWT token for authentication.
Node.js Backend & WebSocket Server (Real-Time Notifications):
The Node.js backend sends a new notification to the WebSocket server whenever there is an update.
The WebSocket server emits the notification to the connected React frontend.
React Frontend (Displaying Notifications):
The React frontend receives the real-time notification from the WebSocket server.
The React frontend displays the new notification in the UI, where the user can see it.
The JWT token includes the userId of the logged-in user, and this userId is used to create a unique socket room for that user, This happens when the frontend establishes a handshake with the backend and we pass the JWT token to the backend. The notification payload also contains this userId, enabling the system to identify which user the notification is intended for and relay it to the appropriate socket room.
Testing the App
First, ensure that both your frontend and backend are up and running.
To start the backend:
npm run server
To start the frontend:
npm run dev
Open Postman and make two POST requests to
http://localhost:4000/login
with the following payloads:{ "username": "John", "password": "password" }
{ "username": "Jane", "password": "password2" }
Please note that these credentials are hardcoded in the /login
endpoint, so you should use these specific credentials unless you have a fully implemented authentication system in your application.
Next, open two browser windows—one in normal mode and the other in incognito mode. In both windows, add a new localStorage value:
"accessToken": your_access_token_from_server
Note that using two different windows is important because localStorage is not shared between them, allowing you to simulate two separate users performing the test.
Next, use Postman as either one of the users and create a new POST request to the
http://localhost:4000/notifications
endpoint.The payload of the request should be one of the following:
{ "userId": "user2", "message": "My Test 1 for Jane's Notification", "Type": "General" }
{ "userId": "user1", "message": "My Test 1 for John's Notification", "Type": "General" }
Please note that the userId
in the payload determines which user will receive the notification. As you can see from the /login
endpoint, these userIds are hardcoded for the respective users.
Also, make sure to add a new header when making this POST request:
authorization: bearer <accessToken>
Otherwise, the server will respond with a "Forbidden" error.
You should now see that the notification has appeared on the frontend for only the user to whom the notification was intended.
You can make additional POST requests to add new notifications and observe how the system behaves for the other user. Ensure that the authorization header includes the accessToken of the other logged-in user, and that the notification payload specifies the userId of the intended recipient.
Conclusion
Building a real-time notification system is a powerful way to keep users engaged and informed. By leveraging the MERN stack and Socket.io, we've created a robust and scalable solution that handles everything from user authentication to real-time updates. Not only does this setup enhance the user experience by delivering instant notifications, but it also provides a solid foundation for building more interactive features in your applications. Whether you're developing a social media platform, an e-commerce site, or any app that benefits from real-time data, the concepts covered in this guide will help you take your project to the next level. Happy coding!