DEV Community

Cover image for Chat-app with video-call tutorial [MERN]
Manas Shinde
Manas Shinde

Posted on

1

Chat-app with video-call tutorial [MERN]

Live project link: Chat-app

Building a Real-Time Chat App with the MERN Stack

Creating a real-time chat application is a great way to enhance your full-stack development skills. This guide provides a quick overview of setting up a MERN-based chat app with Socket.IO for real-time messaging, Cloudinary for profile images, and JWT authentication for user security.

This is not a full tutorial—instead, it summarizes key steps. For detailed implementation, refer to the linked YouTube tutorial below.


Features

  • User Authentication – Secure login and registration with JWT.

Image description

  • Real-Time Messaging – Instant chat functionality with Socket.IO.

  • Responsive UI – Mobile-friendly design with Tailwind CSS.

Image description

  • Image Uploads – Profile images stored using Cloudinary.

Image description

  • Video Calling – WebRTC integration for one-on-one calls.

Image description

Tech Stack

  • Frontend: React.js, Tailwind CSS
  • Backend: Node.js, Express.js, MongoDB
  • Database: MongoDB with Mongoose
  • Real-Time Communication: Socket.IO, WebRTC (for calls)
  • Authentication: JSON Web Tokens (JWT) with bcrypt
  • Image Storage: Cloudinary

1. Setting Up the Backend

Install Dependencies

Create a new project and install required packages:

mkdir mern-chat-app && cd mern-chat-app
npm init -y
npm install express mongoose cors bcryptjs jsonwebtoken socket.io dotenv cloudinary multer
Enter fullscreen mode Exit fullscreen mode

Configure Environment Variables

Create a .env file to store sensitive data:

PORT=5000
MONGO_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Enter fullscreen mode Exit fullscreen mode

Express Server Setup

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');
const authRoutes = require('./routes/authRoutes'); // Example route file
const chatRoutes = require('./routes/chatRoutes');
const socketIo = require('socket.io');
const http = require('http');

dotenv.config();
const app = express();
const server = http.createServer(app);
const io = socketIo(server, { cors: { origin: '*' } });

app.use(cors());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/chat', chatRoutes);

mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then(() => console.log('MongoDB connected'));

const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

2. Authentication (JWT & Mongoose Setup)

User Model (Mongoose Schema)

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});

UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

module.exports = mongoose.model('User', UserSchema);
Enter fullscreen mode Exit fullscreen mode

Auth Routes

const express = require('express');
const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const router = express.Router();

router.post('/register', async (req, res) => {
  const { username, email, password } = req.body;
  try {
    const user = new User({ username, email, password });
    await user.save();
    res.status(201).json({ message: 'User created successfully' });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

3. Setting Up the Frontend

Install React & Dependencies

npx create-react-app client
cd client
npm install axios socket.io-client react-router-dom tailwindcss
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind CSS

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

Edit tailwind.config.js:

module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Import Tailwind into index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

4. Real-Time Chat with Socket.IO

Backend Socket.IO Setup

io.on('connection', (socket) => {
  console.log('New client connected');

  socket.on('sendMessage', (message) => {
    io.emit('message', message);
  });

  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});
Enter fullscreen mode Exit fullscreen mode

Frontend Socket.IO Integration

import io from 'socket.io-client';
const socket = io('http://localhost:5000');

socket.on('message', (message) => {
  console.log(message);
});

const sendMessage = (message) => {
  socket.emit('sendMessage', message);
};
Enter fullscreen mode Exit fullscreen mode

5. Deployment

Once the app is ready, deploy it using:

Example Heroku Deployment:

git init
git add .
git commit -m "Deploy"
heroku create your-app-name
git push heroku main
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building a chat app with MERN & Socket.IO is a great way to practice real-time web development. This guide summarizes the key steps, but for a detailed implementation, check out the YouTube tutorial below:

MERN Stack Project: Realtime Chat App Tutorial

For Video calling implementation 👇

Video Call Feature

For the video call feature, I took a modular approach to keep the code organized and easy to debug—especially important for tackling WebRTC for the first time.

Process Overview

State Management with Zustand:

I created a dedicated Zustand store called useVideoStore to manage all video call-related functionalities. This store encapsulates the video call logic into simple, reusable functions. It handles tasks such as:

  • initializeMedia: Requests access to the user's camera and microphone using the navigator.mediaDevices.getUserMedia API.
  • makeCall: Initiates a video call by sending a call request through Socket.IO.
  • answerCall: Handles answering incoming call requests.
  • handleIncomingCall: Listens for and processes incoming call signals.
  • handleIceCandidates: Manages the exchange of ICE candidates—a critical part of establishing a peer-to-peer WebRTC connection.
  • endCall: Terminates the call and cleans up media streams.

Challenges & Resolutions

  • ICE Candidate Handling:

    I initially got stuck ensuring that ICE candidates were correctly exchanged between peers. Detailed logging and verifying events in the Socket.IO signaling server revealed timing issues. Adjusting the order of operations—ensuring media streams were fully initialized before sending ICE candidates—resolved this problem.

  • Media Stream Initialization:

    Requesting camera and microphone permissions can be tricky. I encountered browser permission issues and compatibility errors. Proper error handling with navigator.mediaDevices.getUserMedia and cross-browser testing helped overcome these challenges.

  • State Synchronization:

    Integrating real-time events with state updates required careful management. Breaking the logic into small, dedicated functions within the Zustand store allowed me to isolate problems and ensure the app’s state accurately reflected the call status.

Example: Zustand Store for Video Calls

Below is a simplified pseudocode version of the Zustand store (useVideoStore) that encapsulates the video call functionalities:

// useVideoStore.js
import create from 'zustand';

const useVideoStore = create((set) => ({
  localStream: null,
  remoteStream: null,
  callInProgress: false,

  // Initialize media: request access to camera and microphone
  initializeMedia: async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ 
        video: true, 
        audio: true 
      });
      set({ localStream: stream });
    } catch (error) {
      console.error('Error accessing media devices:', error);
    }
  },

  // Initiate a call by signaling the target user
  makeCall: (targetId) => {
    // Code to emit a call event via Socket.IO
  },

  // Answer an incoming call
  answerCall: () => {
    // Code to handle answering the call
  },

  // Manage incoming call signals
  handleIncomingCall: (callerInfo) => {
    // Code to update state with incoming call details
  },

  // Handle ICE candidate exchange for establishing a peer-to-peer connection
  handleIceCandidates: (candidate) => {
    // Code to process and send ICE candidates via Socket.IO
  },

  // End the ongoing call and clean up resources
  endCall: () => {
    // Stop local media tracks and reset state
    set({ 
      localStream: null, 
      remoteStream: null, 
      callInProgress: false 
    });
  },
}));

export default useVideoStore;
Enter fullscreen mode Exit fullscreen mode

Integration in the App

In App.jsx, I imported useVideoStore and set up Socket.IO event listeners. This allowed me to tie the video call logic directly to UI components. By leveraging Zustand's simplicity, I could focus on debugging specific functions (like handleIceCandidates) without getting overwhelmed by overall state management.

Advanced Details: ICE, STUN & TURN Servers

Stun and turn server overview

ICE Candidate Exchange with Socket.IO:

WebRTC uses ICE (Interactive Connectivity Establishment) to discover and exchange network information (IP addresses and ports) between peers. Socket.IO serves as the signaling channel, transmitting ICE candidates between peers as they’re gathered. This exchange is essential for establishing a direct connection.

STUN Servers:

A STUN (Session Traversal Utilities for NAT) server helps a client determine its public IP address and port, which is vital when the client is behind a NAT or firewall. In my setup, I used a free public STUN server:

stun:stun.l.google.com:19302
there are more servers you can search which work fine for same N/W

This server is included in the WebRTC configuration to help peers gather their connection candidates.

TURN Servers:

TURN (Traversal Using Relays around NAT) servers relay media streams when a direct peer-to-peer connection cannot be established. This is especially necessary when both peers are behind strict NATs or firewalls.

  • Challenges with TURN:
    • Reliability: Without a TURN server, calls might fail if a direct connection isn’t possible.
    • Setup Complexity: Configuring a TURN server requires a dedicated VPS and software like Coturn (*requires VPS running).
    • Costs: Running your own TURN server may incur costs, although paid services like Twilio or Xirsys are alternatives.

Due to time constraints, I didn’t set up my own TURN server. However, if you require robust connectivity, you can set up a TURN server on your own VPS. Follow this guide for instructions on installing and configuring Coturn.

Final Thoughts

Integrating ICE candidate exchange, STUN, and TURN servers is key to reliable video calling. Free STUN servers are usually sufficient, but TURN servers become critical under challenging network conditions. This modular approach using a Zustand store not only made the process more manageable but also prepared the app to handle real-world network scenarios.

Even though setting up WebRTC for video calls can be challenging, persistence and careful debugging eventually led to a seamless integration that complements the chat functionality and enhances the overall user experience.

Feel free to reach out if you have any questions or need further clarification on any of these steps!

Resources

My Socials

LinkedInGitHubDev.to

Thanks for reading till here please support this blog by liking🙏

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (1)

Collapse
 
manas_31 profile image
Manas Shinde • Edited

for queries mail me here- m50224683@gmail.com

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post