Building a Real-time Chat System with Node.js and Socket.io: Complete Implementation Guide
Introduction
Real-time communication has become essential in modern web applications. Whether you're building a customer support system, collaborative tool, or social platform, implementing chat functionality is often a core requirement. In this comprehensive guide, we'll build a scalable real-time chat system using Node.js, Socket.io, and Redis.
Project Setup and Dependencies
Let's start by setting up our project structure and installing the necessary dependencies:
mkdir realtime-chat
cd realtime-chat
npm init -y
npm install express socket.io redis cors helmet
npm install -D nodemonCreate the following project structure:
realtime-chat/
├── server/
│ ├── index.js
│ ├── models/
│ │ └── Message.js
│ └── utils/
│ └── redisClient.js
├── public/
│ ├── index.html
│ ├── style.css
│ └── client.js
└── package.jsonSetting Up the Express Server with Socket.io
First, let's create our main server file that handles both HTTP requests and WebSocket connections:
// server/index.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const helmet = require('helmet');
const path = require('path');
const redisClient = require('./utils/redisClient');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: process.env.CLIENT_URL || "http://localhost:3000",
methods: ["GET", "POST"]
}
});
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, '../public')));
// Store active users
const activeUsers = new Map();
// Socket.io connection handling
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// Handle user joining
socket.on('join', async (userData) => {
try {
const { username, room } = userData;
// Store user info
activeUsers.set(socket.id, { username, room, socketId: socket.id });
// Join the room
socket.join(room);
// Get recent messages from Redis
const recentMessages = await redisClient.getRecentMessages(room, 50);
socket.emit('recent_messages', recentMessages);
// Notify room about new user
socket.to(room).emit('user_joined', {
username,
message: `${username} joined the chat`,
timestamp: new Date().toISOString()
});
// Send updated user list
const roomUsers = Array.from(activeUsers.values())
.filter(user => user.room === room);
io.to(room).emit('room_users', roomUsers);
} catch (error) {
console.error('Join error:', error);
socket.emit('error', 'Failed to join room');
}
});
// Handle new messages
socket.on('send_message', async (messageData) => {
try {
const user = activeUsers.get(socket.id);
if (!user) return;
const message = {
id: generateMessageId(),
username: user.username,
content: messageData.content,
room: user.room,
timestamp: new Date().toISOString(),
type: messageData.type || 'text'
};
// Store message in Redis
await redisClient.storeMessage(message);
// Broadcast to room
io.to(user.room).emit('new_message', message);
} catch (error) {
console.error('Message error:', error);
socket.emit('error', 'Failed to send message');
}
});
// Handle typing indicators
socket.on('typing', (data) => {
const user = activeUsers.get(socket.id);
if (user) {
socket.to(user.room).emit('user_typing', {
username: user.username,
isTyping: data.isTyping
});
}
});
// Handle disconnection
socket.on('disconnect', () => {
const user = activeUsers.get(socket.id);
if (user) {
// Remove from active users
activeUsers.delete(socket.id);
// Notify room
socket.to(user.room).emit('user_left', {
username: user.username,
message: `${user.username} left the chat`,
timestamp: new Date().toISOString()
});
// Update user list
const roomUsers = Array.from(activeUsers.values())
.filter(u => u.room === user.room);
io.to(user.room).emit('room_users', roomUsers);
}
console.log('User disconnected:', socket.id);
});
});
function generateMessageId() {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
}
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Redis Integration for Message Persistence
Redis provides excellent performance for storing and retrieving chat messages. Here's our Redis client implementation:
// server/utils/redisClient.js
const redis = require('redis');
class RedisClient {
constructor() {
this.client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || undefined
});
this.client.on('error', (err) => {
console.error('Redis error:', err);
});
this.client.on('connect', () => {
console.log('Connected to Redis');
});
}
async storeMessage(message) {
try {
const key = `room:${message.room}:messages`;
await this.client.zadd(
key,
Date.now(),
JSON.stringify(message)
);
// Keep only last 1000 messages per room
await this.client.zremrangebyrank(key, 0, -1001);
} catch (error) {
console.error('Error storing message:', error);
}
}
async getRecentMessages(room, limit = 50) {
try {
const key = `room:${room}:messages`;
const messages = await this.client.zrevrange(
key, 0, limit - 1
);
return messages.map(msg => JSON.parse(msg)).reverse();
} catch (error) {
console.error('Error getting messages:', error);
return [];
}
}
async getRoomStats(room) {
try {
const key = `room:${room}:messages`;
const count = await this.client.zcard(key);
return { messageCount: count };
} catch (error) {
console.error('Error getting room stats:', error);
return { messageCount: 0 };
}
}
}
module.exports = new RedisClient();Frontend Implementation
Now let's create a simple but functional frontend client:
// public/client.js
const socket = io();
let currentUser = null;
let currentRoom = null;
let typingTimeout = null;
class ChatClient {
constructor() {
this.initializeElements();
this.bindEvents();
this.setupSocketListeners();
}
initializeElements() {
this.loginForm = document.getElementById('loginForm');
this.chatContainer = document.getElementById('chatContainer');
this.messagesDiv = document.getElementById('messages');
this.messageInput = document.getElementById('messageInput');
this.sendButton = document.getElementById('sendButton');
this.usersDiv = document.getElementById('users');
this.typingDiv = document.getElementById('typing');
}
bindEvents() {
this.loginForm.addEventListener('submit', (e) => {
e.preventDefault();
this.joinChat();
});
this.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
} else {
this.handleTyping();
}
});
this.sendButton.addEventListener('click', () => {
this.sendMessage();
});
}
setupSocketListeners() {
socket.on('recent_messages', (messages) => {
messages.forEach(msg => this.displayMessage(msg));
});
socket.on('new_message', (message) => {
this.displayMessage(message);
});
socket.on('user_joined', (data) => {
this.displaySystemMessage(data.message, data.timestamp);
});
socket.on('user_left', (data) => {
this.displaySystemMessage(data.message, data.timestamp);
});
socket.on('room_users', (users) => {
this.updateUsersList(users);
});
socket.on('user_typing', (data) => {
this.showTypingIndicator(data.username, data.isTyping);
});
}
joinChat() {
const username = document.getElementById('username').value.trim();
const room = document.getElementById('room').value.trim();
if (username && room) {
currentUser = username;
currentRoom = room;
socket.emit('join', { username, room });
this.loginForm.style.display = 'none';
this.chatContainer.style.display = 'block';
}
}
sendMessage() {
const content = this.messageInput.value.trim();
if (content) {
socket.emit('send_message', {
content: content,
type: 'text'
});
this.messageInput.value = '';
this.stopTyping();
}
}
displayMessage(message) {
const messageEl = document.createElement('div');
messageEl.className = `message ${message.username === currentUser ? 'own' : 'other'}`;
const time = new Date(message.timestamp).toLocaleTimeString();
messageEl.innerHTML = `
`;
this.messagesDiv.appendChild(messageEl);
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight;
}
displaySystemMessage(content, timestamp) {
const messageEl = document.createElement('div');
messageEl.className = 'message system';
const time = new Date(timestamp).toLocaleTimeString();
messageEl.innerHTML = `
${content} at ${time}
`;
this.messagesDiv.appendChild(messageEl);
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight;
}
handleTyping() {
socket.emit('typing', { isTyping: true });
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
this.stopTyping();
}, 1000);
}
stopTyping() {
socket.emit('typing', { isTyping: false });
}
showTypingIndicator(username, isTyping) {
if (isTyping && username !== currentUser) {
this.typingDiv.textContent = `${username} is typing...`;
this.typingDiv.style.display = 'block';
} else {
this.typingDiv.style.display = 'none';
}
}
updateUsersList(users) {
this.usersDiv.innerHTML = 'Online Users
';
users.forEach(user => {
const userEl = document.createElement('div');
userEl.className = 'user';
userEl.textContent = user.username;
if (user.username === currentUser) {
userEl.classList.add('current-user');
}
this.usersDiv.appendChild(userEl);
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize chat client when page loads
document.addEventListener('DOMContentLoaded', () => {
new ChatClient();
});Production Considerations
For production deployment, consider these important aspects:
- Horizontal Scaling: Use Redis adapter for Socket.io to enable multiple server instances
- Rate Limiting: Implement message rate limiting to prevent spam
- Authentication: Add proper user authentication and authorization
- Message Validation: Sanitize and validate all incoming messages
- Error Handling: Implement comprehensive error handling and logging
- Monitoring: Add metrics for connection counts, message rates, and system health
Conclusion
This implementation provides a solid foundation for a real-time chat system. The combination of Node.js, Socket.io, and Redis offers excellent performance and scalability. You can extend this base with features like file sharing, message reactions, user roles, and push notifications to create a full-featured chat application.
Related Posts
Building Secure JWT Authentication with NestJS Guards and Decorators
Learn to implement robust JWT authentication in NestJS using custom guards, decorators, and best security practices.
Building Scalable GraphQL APIs with NestJS: A Practical Guide
Learn to create powerful, type-safe GraphQL APIs using NestJS with practical examples and best practices for scalable applications.
Building Production-Ready REST APIs with FastAPI and Pydantic
Learn how to build robust, type-safe REST APIs using FastAPI and Pydantic with proper validation, documentation, and error handling.