Building Real-Time Chat Applications with Socket.io and Node.js
Introduction
Real-time communication has become a cornerstone of modern web applications. Whether you're building a customer support system, collaborative tools, or social platforms, implementing chat functionality is often essential. In this guide, we'll build a production-ready chat application using Socket.io and Node.js, complete with authentication, room management, and message persistence.
Setting Up the Project
Let's start by creating our Node.js server with the necessary dependencies:
npm init -y
npm install express socket.io mongoose bcryptjs jsonwebtoken cors dotenv
npm install -D nodemonCreate the basic server structure:
// server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: process.env.CLIENT_URL || "http://localhost:3000",
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.json());
// MongoDB connection
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/chatapp');
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Database Models
We'll create models for users, chat rooms, and messages:
// models/User.js
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 },
avatar: { type: String, default: '' },
isOnline: { type: Boolean, default: false },
lastSeen: { type: Date, default: Date.now }
}, { timestamps: true });
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
module.exports = mongoose.model('User', userSchema);// models/Room.js
const mongoose = require('mongoose');
const roomSchema = new mongoose.Schema({
name: { type: String, required: true },
description: { type: String, default: '' },
isPrivate: { type: Boolean, default: false },
members: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
admins: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
}, { timestamps: true });
module.exports = mongoose.model('Room', roomSchema);// models/Message.js
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
content: { type: String, required: true },
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
room: { type: mongoose.Schema.Types.ObjectId, ref: 'Room', required: true },
messageType: { type: String, enum: ['text', 'image', 'file'], default: 'text' },
isEdited: { type: Boolean, default: false },
editedAt: { type: Date }
}, { timestamps: true });
module.exports = mongoose.model('Message', messageSchema);Authentication Middleware
Create middleware to authenticate socket connections:
// middleware/socketAuth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const socketAuth = async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error'));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id).select('-password');
if (!user) {
return next(new Error('User not found'));
}
socket.userId = user._id.toString();
socket.user = user;
next();
} catch (error) {
next(new Error('Authentication error'));
}
};
module.exports = socketAuth;Socket.io Implementation
Now let's implement the core chat functionality:
// socket/chatHandler.js
const Message = require('../models/Message');
const Room = require('../models/Room');
const User = require('../models/User');
class ChatHandler {
constructor(io) {
this.io = io;
this.connectedUsers = new Map();
}
handleConnection(socket) {
console.log(`User ${socket.user.username} connected`);
// Update user online status
this.updateUserStatus(socket.userId, true);
this.connectedUsers.set(socket.userId, socket.id);
// Join user to their rooms
this.joinUserRooms(socket);
// Handle incoming messages
socket.on('send_message', (data) => this.handleMessage(socket, data));
// Handle joining rooms
socket.on('join_room', (roomId) => this.handleJoinRoom(socket, roomId));
// Handle leaving rooms
socket.on('leave_room', (roomId) => this.handleLeaveRoom(socket, roomId));
// Handle typing indicators
socket.on('typing_start', (roomId) => this.handleTyping(socket, roomId, true));
socket.on('typing_stop', (roomId) => this.handleTyping(socket, roomId, false));
// Handle disconnection
socket.on('disconnect', () => this.handleDisconnection(socket));
}
async handleMessage(socket, data) {
try {
const { content, roomId, messageType = 'text' } = data;
// Validate room membership
const room = await Room.findOne({
_id: roomId,
members: socket.userId
});
if (!room) {
socket.emit('error', { message: 'Not authorized to send messages to this room' });
return;
}
// Create and save message
const message = new Message({
content,
sender: socket.userId,
room: roomId,
messageType
});
await message.save();
await message.populate('sender', 'username avatar');
// Emit to room members
this.io.to(roomId).emit('new_message', {
_id: message._id,
content: message.content,
sender: message.sender,
room: message.room,
messageType: message.messageType,
createdAt: message.createdAt
});
} catch (error) {
socket.emit('error', { message: 'Failed to send message' });
}
}
async handleJoinRoom(socket, roomId) {
try {
const room = await Room.findById(roomId);
if (!room || !room.members.includes(socket.userId)) {
socket.emit('error', { message: 'Not authorized to join this room' });
return;
}
socket.join(roomId);
socket.emit('joined_room', { roomId, roomName: room.name });
// Notify others
socket.to(roomId).emit('user_joined', {
userId: socket.userId,
username: socket.user.username
});
} catch (error) {
socket.emit('error', { message: 'Failed to join room' });
}
}
handleTyping(socket, roomId, isTyping) {
socket.to(roomId).emit('user_typing', {
userId: socket.userId,
username: socket.user.username,
isTyping
});
}
async updateUserStatus(userId, isOnline) {
await User.findByIdAndUpdate(userId, {
isOnline,
lastSeen: new Date()
});
}
async handleDisconnection(socket) {
console.log(`User ${socket.user.username} disconnected`);
this.connectedUsers.delete(socket.userId);
await this.updateUserStatus(socket.userId, false);
}
}
module.exports = ChatHandler;Integrating Everything
Finally, let's wire everything together in our main server file:
// Add to server.js
const socketAuth = require('./middleware/socketAuth');
const ChatHandler = require('./socket/chatHandler');
// Apply authentication middleware
io.use(socketAuth);
// Initialize chat handler
const chatHandler = new ChatHandler(io);
// Handle socket connections
io.on('connection', (socket) => {
chatHandler.handleConnection(socket);
});Performance Optimization Tips
1. Use Redis for Scaling: When scaling to multiple servers, use Redis adapter for Socket.io to share connections across instances.
2. Implement Rate Limiting: Prevent spam by limiting message frequency per user.
3. Message Pagination: Load messages in chunks to improve initial load times.
4. Connection Pooling: Optimize database connections for better performance.
Conclusion
This implementation provides a solid foundation for real-time chat applications. You can extend it with features like file uploads, message reactions, push notifications, and advanced moderation tools. Remember to implement proper error handling, logging, and monitoring in production environments.
Related Posts
Building Type-Safe APIs with GraphQL Code-First Approach in NestJS
Learn how to create robust, type-safe GraphQL APIs using NestJS's code-first approach with TypeScript decorators and automatic schema generation.
Implementing Clean Architecture with NestJS: Building Scalable Enterprise Applications
Learn how to implement Clean Architecture principles in NestJS applications for better maintainability and testability.
Building a Real-Time Chat Application with Socket.IO and Express
Learn to build a scalable real-time chat app using Socket.IO, Express, and modern JavaScript patterns with authentication and room management.