Project Overview
This real-time chat application demonstrates modern web communication patterns using WebSockets. The project showcases full-stack development skills, real-time data synchronization, and scalable architecture design.
Key Features
- Real-time messaging with instant delivery
- Multiple chat rooms with dynamic creation
- Typing indicators to show when users are composing messages
- User presence tracking (online/offline status)
- Message history with MongoDB persistence
- User authentication with JWT tokens
- Responsive design that works on mobile and desktop
Technical Implementation
Backend Architecture
The backend is built with Node.js and Express, using Socket.io for WebSocket management:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: process.env.CLIENT_URL,
methods: ['GET', 'POST']
}
});
// Socket.io connection handling
io.on('connection', (socket) => {
console.log('New client connected:', socket.id);
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', socket.id);
});
socket.on('send-message', async (data) => {
const message = await Message.create({
content: data.content,
userId: data.userId,
roomId: data.roomId,
timestamp: new Date()
});
io.to(data.roomId).emit('receive-message', message);
});
socket.on('typing', (data) => {
socket.to(data.roomId).emit('user-typing', {
userId: data.userId,
username: data.username
});
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
Frontend Implementation
The React frontend uses hooks for state management and Socket.io client for real-time updates:
import { useEffect, useState } from 'react';
import io, { Socket } from 'socket.io-client';
interface Message {
id: string;
content: string;
userId: string;
username: string;
timestamp: Date;
}
export const ChatRoom = ({ roomId, userId, username }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [typingUsers, setTypingUsers] = useState<string[]>([]);
useEffect(() => {
const newSocket = io(process.env.REACT_APP_SERVER_URL);
setSocket(newSocket);
newSocket.emit('join-room', roomId);
newSocket.on('receive-message', (message: Message) => {
setMessages(prev => [...prev, message]);
});
newSocket.on('user-typing', ({ username }) => {
setTypingUsers(prev => [...prev, username]);
setTimeout(() => {
setTypingUsers(prev => prev.filter(u => u !== username));
}, 3000);
});
return () => {
newSocket.disconnect();
};
}, [roomId]);
const sendMessage = () => {
if (socket && inputValue.trim()) {
socket.emit('send-message', {
content: inputValue,
userId,
username,
roomId
});
setInputValue('');
}
};
const handleTyping = () => {
if (socket) {
socket.emit('typing', { userId, username, roomId });
}
};
return (
<div className="chat-room">
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className="message">
<strong>{msg.username}:</strong> {msg.content}
</div>
))}
</div>
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
</div>
)}
<input
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
handleTyping();
}}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
</div>
);
};
Challenges & Solutions
Challenge 1: Message Ordering
Problem: Messages could arrive out of order due to network latency.
Solution: Implemented server-side timestamps and client-side sorting to ensure messages always display in chronological order.
Challenge 2: Connection Stability
Problem: Users would lose connection on network interruptions.
Solution: Added automatic reconnection logic with exponential backoff and message queue to resend failed messages.
Challenge 3: Scalability
Problem: Single server couldn’t handle thousands of concurrent connections.
Solution: Implemented Redis adapter for Socket.io to enable horizontal scaling across multiple server instances.
Lessons Learned
- WebSocket lifecycle management is crucial - proper cleanup prevents memory leaks
- Optimistic UI updates improve perceived performance
- Rate limiting is essential to prevent spam and abuse
- Connection state management requires careful handling of edge cases
- Testing real-time features requires specialized tools and strategies
Future Enhancements
- End-to-end encryption for private messages
- File sharing and image uploads
- Voice and video calling
- Message reactions and threading
- Push notifications for offline users
Try It Out
Check out the live demo or explore the source code on GitHub.