Building a Real-Time Chat Application with React, WebSockets, and MongoDB

23/06/2025

Building a Real-Time Chat Application with React, WebSockets, and MongoDB

A full-stack real-time chat application built with React.js, Node.js, WebSockets, and MongoDB. It allows users to send and receive instant messages with live updates, persistent chat history, and a clean UI. Ideal for learning real-time communication and full-stack integration.

Building a Real-Time Chat Application with React, WebSockets, and MongoDB

Chat applications are ubiquitous in today's digital world, enabling instant communication between individuals and groups. The magic behind their real-time nature often lies in WebSockets, a communication protocol that provides a full-duplex communication channel over a single TCP connection. Unlike traditional HTTP requests (which are stateless and often short-lived), WebSockets allow the server to push data to the client without the client explicitly requesting it, making them ideal for live updates like chat messages.

In this blog post, we'll walk through building a simple real-time chat application using:

  • React.js: For building a dynamic and interactive user interface on the frontend.
  • Node.js & Express: For creating a robust and scalable backend server.
  • WebSockets (using the ws library): For establishing real-time communication between the client and server.
  • MongoDB: For storing chat messages persistently in a NoSQL database.

1. Understanding the Core Concepts

Before we dive into coding, let's briefly understand the key components:

  • WebSockets: A protocol providing full-duplex communication channels over a single TCP connection. This means both client and server can send and receive data simultaneously.
  • Node.js: A JavaScript runtime built on Chrome's V8 JavaScript engine, perfect for building fast, scalable network applications.
  • Express.js: A minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
  • MongoDB: A popular NoSQL document database that stores data in flexible, JSON-like documents, making it easy to integrate with JavaScript applications.
  • React.js: A JavaScript library for building user interfaces, known for its component-based architecture and declarative approach.

2. Prerequisites

Before you begin, ensure you have the following installed on your system:

  • Node.js and npm: Download and install from nodejs.org.
  • MongoDB: Download and install from mongodb.com. You'll also need to have a MongoDB instance running (either locally or a cloud-hosted one like MongoDB Atlas).
  • Text Editor/IDE: Visual Studio Code, Sublime Text, or any editor of your choice.

3. Backend Setup (Node.js, Express & WebSockets)

We'll start by building the backend server that will handle WebSocket connections, manage messages, and interact with the database.

Step 3.1: Initialize the Node.js Project

Create a new folder for your project and initialize a Node.js project:

mkdir chat-app
cd chat-app
mkdir backend
cd backend
npm init -y
  

Step 3.2: Install Dependencies

Install Express, ws (for WebSockets), and mongoose (for MongoDB interaction):

npm install express ws mongoose dotenv
  

dotenv will be used to manage environment variables (like MongoDB connection string).

Step 3.3: Create the Server File (server.js)

In the backend folder, create a file named server.js:

// backend/server.js
require('dotenv').config();
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const mongoose = require('mongoose');

const app = express();
const port = process.env.PORT || 8080;

// --- MongoDB Connection ---
const mongoURI = process.env.MONGO_URI;
mongoose.connect(mongoURI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));

// Define Message Schema
const MessageSchema = new mongoose.Schema({
    sender: { type: String, required: true },
    content: { type: String, required: true },
    timestamp: { type: Date, default: Date.now },
});

const Message = mongoose.model('Message', MessageSchema);

// --- HTTP Server Setup ---
const server = http.createServer(app);

// Basic Express route (optional)
app.get('/', (req, res) => {
    res.send('Chat App Backend is Running!');
});

// --- WebSocket Server Setup ---
const wss = new WebSocket.Server({ server });

const clients = new Set();

wss.on('connection', ws => {
    console.log('Client connected');
    clients.add(ws);

    Message.find().sort({ timestamp: 1 }).limit(100)
        .then(messages => {
            messages.forEach(msg => {
                ws.send(JSON.stringify(msg));
            });
        })
        .catch(err => console.error('Error fetching past messages:', err));

    ws.on('message', async message => {
        console.log(`Received message: ${message}`);
        try {
            const parsedMessage = JSON.parse(message);
            const newMessage = new Message({
                sender: parsedMessage.sender || 'Anonymous',
                content: parsedMessage.content,
                timestamp: new Date()
            });
            await newMessage.save();
            console.log('Message saved to DB:', newMessage);

            const messageToBroadcast = JSON.stringify(newMessage);
            clients.forEach(client => {
                if (client.readyState === WebSocket.OPEN) {
                    client.send(messageToBroadcast);
                }
            });
        } catch (error) {
            console.error('Error processing message:', error);
            if (ws.readyState === WebSocket.OPEN) {
                ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
            }
        }
    });

    ws.on('close', () => {
        console.log('Client disconnected');
        clients.delete(ws);
    });

    ws.on('error', error => {
        console.error('WebSocket error:', error);
    });
});

server.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
    console.log(`WebSocket server is running on ws://localhost:${port}`);
});
  

Step 3.4: Create .env File

In the backend folder, create a .env file and add your MongoDB connection string:

# backend/.env
MONGO_URI=your_mongodb_connection_string
PORT=8080
  

Step 3.5: Run the Backend Server

cd backend
node server.js
  

You should see "MongoDB connected successfully!" and "Server is running on http://localhost:8080" in your console.

4. Frontend Setup (React.js)

Now, let's build the React application that will serve as our chat client.

Step 4.1: Create a React App

Open a new terminal, navigate back to your chat-app root directory, and create a new React app:

cd ..  # Go back to chat-app root
npx create-react-app frontend
cd frontend
  

Step 4.2: Clean Up and Structure

Remove unnecessary files from src (like App.test.js, logo.svg, reportWebVitals.js, setupTests.js). Your src folder should primarily contain:

  • index.js
  • App.js
  • index.css (or App.css)

Step 4.3: Update src/index.css

Add some basic global styles for better aesthetics:

/* src/index.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');

body {
    margin: 0;
    font-family: 'Inter', sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    background-color: #f0f2f5;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    padding: 20px;
    box-sizing: border-box;
}

#root {
    width: 100%;
    max-width: 800px;
}
  

Step 4.4: Create the ChatApp Component (src/App.js)

This will be the main React component managing WebSocket connections, user input, and rendering messages:

// frontend/src/App.js
import React, { useState, useEffect, useRef } from 'react';
import './App.css'; // We'll create this CSS file next

function App() {
    const [messages, setMessages] = useState([]);
    const [inputMessage, setInputMessage] = useState('');
    const [username, setUsername] = useState('');
    const [isUsernameSet, setIsUsernameSet] = useState(false);
    const ws = useRef(null);
    const messagesEndRef = useRef(null);

    const WEBSOCKET_URL = 'ws://localhost:8080';

    useEffect(() => {
        if (!isUsernameSet) return;

        ws.current = new WebSocket(WEBSOCKET_URL);

        ws.current.onopen = () => console.log('WebSocket connection opened');

        ws.current.onmessage = event => {
            try {
                const receivedMessage = JSON.parse(event.data);
                setMessages(prev => [...prev, receivedMessage]);
            } catch (error) {
                console.error('Failed to parse message:', error);
            }
        };

        ws.current.onclose = () => console.log('WebSocket closed');

        ws.current.onerror = error => console.error('WebSocket error:', error);

        return () => {
            if (ws.current?.readyState === WebSocket.OPEN) ws.current.close();
        };
    }, [isUsernameSet]);



    useEffect(() => {
        messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [messages]);

    const sendMessage = (e) => {
        e.preventDefault();
        if (inputMessage.trim() && ws.current?.readyState === WebSocket.OPEN) {
            const messagePayload = {
                sender: username,
                content: inputMessage.trim(),
            };
            ws.current.send(JSON.stringify(messagePayload));
            setInputMessage('');
        }
    };

    const handleUsernameSubmit = (e) => {
        e.preventDefault();
        if (username.trim()) setIsUsernameSet(true);
    };

    if (!isUsernameSet) {
        return (
            <div className="chat-container username-screen">
                <h1 className="chat-title">Welcome to Chat!</h1>
                <form onSubmit={handleUsernameSubmit} className="username-form">
                    <input
                        type="text"
                        placeholder="Enter your username"
                        value={username}
                        onChange={(e) => setUsername(e.target.value)}
                        required
                        className="username-input"
                    />
                    <button type="submit" className="set-username-button">
                        Start Chatting
                    </button>
                </form>
            </div>
        );
    }


    return (
        <div className="chat-container">
            <h1 className="chat-title">Real-Time Chat</h1>
            <div className="messages-window">
                {messages.map((msg, index) => (
                    <div key={index} className={`message ${msg.sender === username ? 'sent' : 'received'}`}>
                        <div className="message-header">
                            <span className="message-sender">{msg.sender}</span>
                            <span className="message-time">{new Date(msg.timestamp).toLocaleTimeString()}</span>
                        </div>
                        <div className="message-content">{msg.content}</div>
                    </div>
                ))}
                <div ref={messagesEndRef} />
            </div>
            <form onSubmit={sendMessage} className="message-input-form">
                <input
                    type="text"
                    placeholder="Type your message..."
                    value={inputMessage}
                    onChange={(e) => setInputMessage(e.target.value)}
                    className="message-input"
                />
                <button type="submit" className="send-button">
                    Send
                </button>
            </form>
            <div className="current-user">Logged in as: <strong>{username}</strong></div>
        </div>
    );
}

export default App;

Step 4.5: Update src/App.css

Add the following styles to design your chat interface:

/* src/App.css */
.chat-container {
    background-color: #fff;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
    padding: 30px;
    display: flex;
    flex-direction: column;
    max-height: 90vh; /* Limit height to prevent excessive scrolling */
    overflow: hidden; /* Hide scrollbar for the container itself */
    width: 100%;
}

.chat-title {
    text-align: center;
    color: #333;
    margin-bottom: 25px;
    font-size: 2.2em;
    font-weight: 700;
}

/* Username screen styling */
.username-screen {
    align-items: center;
    justify-content: center;
    height: 50vh; /* Center content vertically in this screen */
}

.username-form {
    display: flex;
    flex-direction: column;
    gap: 15px;
    width: 100%;
    max-width: 300px;
}

.username-input {
    padding: 12px 15px;
    border: 1px solid #ddd;
    border-radius: 8px;
    font-size: 1em;
    outline: none;
    transition: border-color 0.3s ease;
}

.username-input:focus {
    border-color: #007bff;
}

.set-username-button {
    background-color: #007bff;
    color: white;
    border: none;
    padding: 12px 20px;
    border-radius: 8px;
    font-size: 1.1em;
    cursor: pointer;
    transition: background-color 0.3s ease, transform 0.2s ease;
}

.set-username-button:hover {
    background-color: #0056b3;
    transform: translateY(-2px);
}

/* Messages Window */
.messages-window {
    flex-grow: 1; /* Allows it to take available space */
    overflow-y: auto; /* Enable scrolling for messages */
    padding-right: 10px; /* Space for scrollbar */
    margin-bottom: 20px;
    border: 1px solid #eee;
    border-radius: 8px;
    padding: 15px;
    background-color: #f9fafa;
    min-height: 300px; /* Minimum height for the message window */
}

.message {
    margin-bottom: 15px;
    padding: 10px 15px;
    border-radius: 10px;
    max-width: 80%; /* Limit message bubble width */
    word-wrap: break-word; /* Break long words */
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.message.sent {
    background-color: #dcf8c6; /* Light green for sent messages */
    align-self: flex-end; /* Align to the right */
    margin-left: auto; /* Push to the right */
    border-bottom-right-radius: 2px; /* Small corner for sent */
}

.message.received {
    background-color: #e0e0e0; /* Light gray for received messages */
    align-self: flex-start; /* Align to the left */
    margin-right: auto; /* Push to the left */
    border-bottom-left-radius: 2px; /* Small corner for received */
}

.message-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 5px;
    font-size: 0.85em;
    color: #666;
}

.message-sender {
    font-weight: 500;
    color: #007bff; /* Highlight sender */
}

.message-content {
    font-size: 1em;
    line-height: 1.4;
    color: #333;
}

/* Message Input Form */
.message-input-form {
    display: flex;
    gap: 10px;
}

.message-input {
    flex-grow: 1;
    padding: 12px 15px;
    border: 1px solid #ddd;
    border-radius: 8px;
    font-size: 1em;
    outline: none;
    transition: border-color 0.3s ease;
}

.message-input:focus {
    border-color: #007bff;
}

.send-button {
    background-color: #28a745; /* Green send button */
    color: white;
    border: none;
    padding: 12px 20px;
    border-radius: 8px;
    font-size: 1em;
    cursor: pointer;
    transition: background-color 0.3s ease, transform 0.2s ease;
}

.send-button:hover {
    background-color: #218838;
    transform: translateY(-2px);
}

.current-user {
    text-align: right;
    margin-top: 15px;
    font-size: 0.9em;
    color: #555;
}

/* Responsive Adjustments */
@media (max-width: 768px) {
    .chat-container {
        padding: 20px;
        max-width: 95%;
    }

    .chat-title {
        font-size: 1.8em;
    }

    .username-form {
        max-width: 90%;
    }

    .message-input-form {
        flex-direction: column;
        gap: 15px;
    }

    .send-button {
        width: 100%;
        padding: 15px;
    }
}

  

Step 4.6: Run the Frontend App

Make sure your backend server is still running. Open a new terminal, navigate to the frontend directory and start the app:

cd frontend
npm start
  

This will open your chat application in the browser, typically at http://localhost:3000.

5. How It All Connects

  • User Enters Username: The React app first prompts the user for a username. Once set, it attempts to establish a WebSocket connection.
  • WebSocket Connection: The React app's useEffect hook connects to ws://localhost:8080.
  • Backend Handles Connection:
    • The Node.js wss.on('connection') event fires.
    • The server adds the new client’s WebSocket instance to its clients set.
    • It fetches the last 100 messages from MongoDB and sends them individually to the newly connected client, ensuring new users see chat history.
  • Sending Messages:
    • When a user types a message in React and clicks "Send", the sendMessage function sends a JSON string {"{"}"sender": "username", "content": "message"{"}"} through the WebSocket using ws.current.send().
  • Backend Receives and Broadcasts:
    • The Node.js ws.on('message') event receives the message from the client.
    • It parses the message and saves it to the messages collection in MongoDB.
    • After saving, the server iterates through all connected clients and broadcasts the saved message (now including _id and timestamp) to everyone.
  • Frontend Receives Messages:
    • Each connected React client’s ws.current.onmessage event listener receives the broadcasted message.
    • It parses the JSON message and updates the messages state in React.
    • The React UI re-renders, displaying the new message.
    • The useEffect hook for scrolling ensures the chat window always shows the latest message.
  • Disconnection: When a client closes the browser or tab, the ws.on('close') event fires on the server, and the client's WebSocket instance is removed from the clients set.

6. Conclusion and Next Steps

You've successfully built a basic real-time chat application using React, Node.js, WebSockets, and MongoDB! This project demonstrates fundamental concepts of full-stack development, real-time communication, and data persistence.

Here are some ideas for improving and expanding your chat app:

  • User Authentication: Implement user registration and login to manage distinct user identities. You could use JWT (JSON Web Tokens) for this.
  • Chat Rooms/Channels: Allow users to create and join different chat rooms, managing separate message streams and broadcasting per room.
  • Typing Indicators: Show when other users are typing.
  • Emojis/Rich Text: Add support for emojis and basic text formatting.
  • File Sharing: Allow users to share images or other files.
  • Read Receipts: Indicate when messages have been read by recipients.
  • Error Handling and UI Feedback: Provide visual feedback for network errors or message delivery failures.
  • Deployment: Deploy your backend to a cloud platform (e.g., Heroku, AWS, DigitalOcean) and your frontend (e.g., Netlify, Vercel).
  • Scalability: For larger-scale apps, use a more robust WebSocket library like Socket.IO which offers built-in support for rooms, reconnection, and more.

This project is a solid foundation for building more complex real-time applications. Happy coding!


This real-time chat application is a full-stack project developed using React.js on the frontend, Node.js and Express for the backend, WebSockets for live bi-directional communication, and MongoDB for persistent data storage.

Users begin by entering a username and are instantly connected to a WebSocket server. The system displays the latest 100 chat messages to ensure new users can view past conversation history. Messages are broadcast to all connected clients in real-time, and each one is saved in the database with timestamps for future retrieval.

The frontend React app uses state hooks and effects to manage the user interface and maintain a live message feed. A smooth user experience is achieved through auto-scrolling, responsive layouts, and status displays.

The backend WebSocket server, built with the ws library, handles all messaging logic including broadcasting, saving to MongoDB, and managing client connections.

This project is perfect for those looking to understand:

  • WebSocket-based real-time communication

  • Full-stack app architecture

  • MongoDB schema design and persistence

  • React state and lifecycle management

Additional features like user authentication, chat rooms, emojis, and media sharing can easily be integrated to enhance this base further.