# Building a Real-Time Notification System with MERN Stack and Socket.io: A Step-by-Step Guide

## 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](https://adeesh.hashnode.dev/fullstack-app-setup) to get your application up and running quickly. The accompanying code is available on [GitHub](https://github.com/adeeshsharma/fullstack-setup), 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**

1. Visit [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) and sign up for a free account.
    
2. 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**

1. On the Atlas dashboard, click on the **"Build a Cluster"** button.
    
2. Choose the cloud provider and region for your cluster. For free-tier clusters, you can select the default settings.
    
3. Click **"Create Cluster"**. This process may take a few minutes.
    

**Step 3: Whitelist Your IP Address**

1. After your cluster is created, you’ll need to whitelist your IP address to connect to the cluster.
    
2. Go to the **"Network Access"** tab on the left sidebar.
    
3. Click on **"Add IP Address"**.
    
4. 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).
    
5. Click **"Confirm"** to save your settings.
    

**Step 4: Create a Database User**

1. Go to the **"Database Access"** tab on the left sidebar.
    
2. Click on **"Add New Database User"**.
    
3. Choose a username and password for your database user.
    
4. Under **"Database User Privileges"**, select **"Atlas Admin"** for full access.
    
5. Click **"Add User"** to create the user.
    

**Step 5: Get Your Connection String**

1. Return to the **"Clusters"** view and click on **"Connect"** for your cluster.
    
2. In the popup window, select **"Connect Your Application"**.
    
3. Choose **"Node.js"** as your driver and select the latest version.
    
4. You’ll be provided with a connection string. It will look something like this:
    
    ```markdown
    mongodb+srv://<username>:<password>@cluster0.mongodb.net/?retryWrites=true&w=majority
    ```
    
5. Replace `<username>` and `<password>` with the username and password you created earlier.
    
6. 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](http://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.

1. **Initialize a new Node.js project:**
    
    ```bash
    mkdir notification-system
    cd notification-system
    npm init -y
    ```
    
2. **Install the necessary dependencies:**
    
    ```bash
    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`](http://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.
        
3. **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.

1. **Create** `middleware.js` for handling JWT authentication:
    
    ```javascript
    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 the `Authorization` 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 the `auth` object in the socket handshake and attaches the user data to the socket object.
        
2. **Create a** `.env` file to store sensitive information:
    
    ```javascript
    JWT_SECRET=your_jwt_secret_key
    MONGO_URI=your_mongo_uri
    ```
    
    Replace `your_jwt_secret_key` with a secret key of your choice and `your_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.

1. **Create** `index.js` and add the following code:
    
    ```javascript
    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](http://Socket.IO), and provides the necessary API endpoints to interact with the notification system.
    
    **Importing Required Modules:**
    
    ```javascript
    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**](http://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:**
    
    ```javascript
    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 into `process.env`, allowing you to securely manage sensitive information like your JWT secret and MongoDB URI.
        
    
    **Connecting to MongoDB:**
    
    ```javascript
    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. The `useNewUrlParser` and `useUnifiedTopology` options ensure that you're using the latest MongoDB driver features and connection handling.
        
    
    **Defining the Notification Schema and Model:**
    
    ```javascript
    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`, and `type`. The `timestamp` 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:**
    
    ```javascript
    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**](http://Socket.IO) **for Real-Time Communication:**
    
    ```javascript
    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](http://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:**
    
    ```javascript
    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 the `socket` object. Here, the `userId` 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:
    
    ```javascript
    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:
    
    ```javascript
    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**](http://notification.save)**();** The notification is saved to the MongoDB database.
        
    * [**io.to**](http://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:**
    
    ```javascript
    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` and `username`) 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](https://adeesh.hashnode.dev/fullstack-app-setup), 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`

```javascript
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**

```javascript
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` and `useState` hooks to manage component state and side effects.
    
* [**socket.io**](http://socket.io)**\-client:** This library enables WebSocket communication between the frontend and backend.
    

**Setting Up the WebSocket Connection**

```javascript
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`](http://localhost:4000). The `auth` object contains the `token`, which is passed to the backend for authentication during the WebSocket handshake.
    

**Creating the Main Component**

```javascript
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**

```javascript
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 the `Authorization` header as `Bearer <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**

```javascript
// 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 the `notifications` state array using the `setNotifications` function.
    
* **Cleanup:** The return statement in `useEffect` ensures that the event listener is removed when the component unmounts, preventing memory leaks.
    

**Rendering the UI**

```javascript
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, while `SectionContainer` 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 the `notifications` array and renders each notification message inside a styled list item (`NotificationItem`).
    

**Styled Components**

```javascript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1723754894279/5bc5ad1a-5a01-4492-bf63-57231c9fb4cc.png align="center")

* **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

1. 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`
        
2. Open Postman and make two POST requests to [`http://localhost:4000/login`](http://localhost:4000/login) with the following payloads:
    
    1. ```json
        {
            "username": "John",
            "password": "password"
        }
        ```
        
    2. ```json
        {
            "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.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1723809435501/daa5ab92-5bc4-459c-a9fb-9f5508bf7803.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1723809465988/ed2a2fd9-39ef-4f8a-9667-c7e9d50bac47.png align="center")

3. Next, open two browser windows—one in normal mode and the other in incognito mode. In both windows, add a new localStorage value:
    
    ```json
    "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.
    
4. Next, use Postman as either one of the users and create a new POST request to the [`http://localhost:4000/notifications`](http://localhost:4000/notifications) endpoint.
    
    The payload of the request should be one of the following:
    
    1. ```json
        {
            "userId": "user2",
            "message": "My Test 1 for Jane's Notification",
            "Type": "General"
        }
        ```
        
    2. ```json
        {
            "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:

```json
authorization: bearer <accessToken>
```

Otherwise, the server will respond with a "Forbidden" error.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1723810535845/4b55555c-81fb-4b92-8597-246fb84134ff.png align="center")

5. You should now see that the notification has appeared on the frontend for only the user to whom the notification was intended.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1723810772561/b1cf3c12-b0c3-4f90-9cdc-a131d1603770.png align="center")
    
    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](http://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!

## Github

[https://github.com/adeeshsharma/mern-notification-system](https://github.com/adeeshsharma/mern-notification-system)
