Real-time Chat Application
Complete example of building a real-time chat application using Socket.IO with Response Handler.
Server Setup
Express + Socket.IO Server
javascript
// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { quickSetup, quickSocketSetup } = require('response-handler');
const cors = require('cors');
const app = express();
const httpServer = createServer(app);
// CORS configuration
const corsOptions = {
origin:
process.env.NODE_ENV === 'production' ? ['https://yourapp.com'] : ['http://localhost:3000'],
credentials: true,
};
app.use(cors(corsOptions));
app.use(express.json());
// Response Handler for REST endpoints
app.use(
quickSetup({
enableLogging: true,
logLevel: 'info',
environment: process.env.NODE_ENV || 'development',
}),
);
// Socket.IO setup
const io = new Server(httpServer, {
cors: corsOptions,
transports: ['websocket', 'polling'],
});
// Socket.IO Response Handler
io.use(
quickSocketSetup({
enableLogging: true,
logLevel: 'info',
environment: process.env.NODE_ENV || 'development',
}),
);
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = { app, io, httpServer };User Management
User Session Management
javascript
// models/User.js
class ChatUser {
constructor(id, username, socketId) {
this.id = id;
this.username = username;
this.socketId = socketId;
this.joinedAt = new Date();
this.lastSeen = new Date();
this.isOnline = true;
}
toJSON() {
return {
id: this.id,
username: this.username,
joinedAt: this.joinedAt,
lastSeen: this.lastSeen,
isOnline: this.isOnline,
};
}
}
// Store for connected users
class UserManager {
constructor() {
this.users = new Map(); // socketId -> User
this.usersByName = new Map(); // username -> User
}
addUser(socketId, username) {
// Check if username is already taken
if (this.usersByName.has(username)) {
return { error: 'Username already taken' };
}
const user = new ChatUser(Date.now().toString(), username, socketId);
this.users.set(socketId, user);
this.usersByName.set(username, user);
return { user };
}
removeUser(socketId) {
const user = this.users.get(socketId);
if (user) {
this.users.delete(socketId);
this.usersByName.delete(user.username);
return user;
}
return null;
}
getUser(socketId) {
return this.users.get(socketId);
}
getUserByName(username) {
return this.usersByName.get(username);
}
getAllUsers() {
return Array.from(this.users.values()).map((user) => user.toJSON());
}
updateLastSeen(socketId) {
const user = this.users.get(socketId);
if (user) {
user.lastSeen = new Date();
}
}
}
const userManager = new UserManager();
module.exports = { ChatUser, UserManager, userManager };Room Management
Chat Rooms
javascript
// models/Room.js
class ChatRoom {
constructor(id, name, createdBy) {
this.id = id;
this.name = name;
this.createdBy = createdBy;
this.createdAt = new Date();
this.members = new Set();
this.messages = [];
this.isPrivate = false;
this.maxMembers = 100;
}
addMember(userId) {
if (this.members.size >= this.maxMembers) {
return { error: 'Room is full' };
}
this.members.add(userId);
return { success: true };
}
removeMember(userId) {
this.members.delete(userId);
}
addMessage(message) {
this.messages.push({
id: Date.now().toString(),
...message,
timestamp: new Date(),
});
// Keep only last 100 messages in memory
if (this.messages.length > 100) {
this.messages = this.messages.slice(-100);
}
}
getRecentMessages(limit = 50) {
return this.messages.slice(-limit);
}
toJSON() {
return {
id: this.id,
name: this.name,
createdBy: this.createdBy,
createdAt: this.createdAt,
memberCount: this.members.size,
isPrivate: this.isPrivate,
maxMembers: this.maxMembers,
};
}
}
class RoomManager {
constructor() {
this.rooms = new Map();
this.createDefaultRooms();
}
createDefaultRooms() {
this.createRoom('general', 'General Chat', 'system');
this.createRoom('random', 'Random', 'system');
}
createRoom(id, name, createdBy) {
if (this.rooms.has(id)) {
return { error: 'Room already exists' };
}
const room = new ChatRoom(id, name, createdBy);
this.rooms.set(id, room);
return { room };
}
getRoom(id) {
return this.rooms.get(id);
}
getAllRooms() {
return Array.from(this.rooms.values()).map((room) => room.toJSON());
}
deleteRoom(id) {
return this.rooms.delete(id);
}
}
const roomManager = new RoomManager();
module.exports = { ChatRoom, RoomManager, roomManager };Socket Event Handlers
Connection Management
javascript
// handlers/connectionHandler.js
const { userManager } = require('../models/User');
const { roomManager } = require('../models/Room');
function handleConnection(io) {
io.on('connection', (socket) => {
console.log(`Socket connected: ${socket.id}`);
// User join
socket.on('user:join', (data) => {
try {
const { username } = data;
if (!username || username.trim().length < 2) {
return socket.badRequest(
{ username: 'Username must be at least 2 characters' },
'Invalid username',
);
}
const result = userManager.addUser(socket.id, username.trim());
if (result.error) {
return socket.conflict({ username }, result.error);
}
const user = result.user;
// Join default room
socket.join('general');
roomManager.getRoom('general').addMember(user.id);
// Notify user
socket.ok(user.toJSON(), 'Successfully joined chat');
// Broadcast to room
socket.to('general').emit('user:joined', {
user: user.toJSON(),
message: `${username} joined the chat`,
});
// Send room info
socket.emit('room:joined', {
roomId: 'general',
roomName: 'General Chat',
members: roomManager.getRoom('general').members.size,
recentMessages: roomManager.getRoom('general').getRecentMessages(20),
});
} catch (error) {
socket.error(error, 'Failed to join chat');
}
});
// Handle disconnection
socket.on('disconnect', () => {
try {
const user = userManager.removeUser(socket.id);
if (user) {
// Remove from all rooms
for (const room of roomManager.rooms.values()) {
room.removeMember(user.id);
}
// Notify others
socket.broadcast.emit('user:left', {
user: user.toJSON(),
message: `${user.username} left the chat`,
});
console.log(`User ${user.username} disconnected`);
}
} catch (error) {
console.error('Error handling disconnect:', error);
}
});
});
}
module.exports = handleConnection;Message Handling
javascript
// handlers/messageHandler.js
const { userManager } = require('../models/User');
const { roomManager } = require('../models/Room');
function handleMessages(io) {
io.on('connection', (socket) => {
// Send message
socket.on('message:send', (data) => {
try {
const { roomId, content, type = 'text' } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
if (!room.members.has(user.id)) {
return socket.forbidden({}, 'Not a member of this room');
}
// Validate message
if (!content || content.trim().length === 0) {
return socket.badRequest(
{ content: 'Message cannot be empty' },
'Invalid message content',
);
}
if (content.length > 1000) {
return socket.badRequest(
{ content: 'Message too long' },
'Message must be under 1000 characters',
);
}
// Create message
const message = {
id: Date.now().toString(),
userId: user.id,
username: user.username,
content: content.trim(),
type,
roomId,
timestamp: new Date(),
};
// Add to room
room.addMessage(message);
// Update user activity
userManager.updateLastSeen(socket.id);
// Send to all room members
io.to(roomId).emit('message:received', message);
// Confirm to sender
socket.ok({ messageId: message.id }, 'Message sent successfully');
} catch (error) {
socket.error(error, 'Failed to send message');
}
});
// Edit message
socket.on('message:edit', (data) => {
try {
const { messageId, content, roomId } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
const message = room.messages.find((m) => m.id === messageId);
if (!message) {
return socket.notFound({}, 'Message not found');
}
if (message.userId !== user.id) {
return socket.forbidden({}, 'Can only edit your own messages');
}
// Check if message is too old (5 minutes)
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
if (message.timestamp < fiveMinutesAgo) {
return socket.forbidden({}, 'Message too old to edit');
}
// Update message
message.content = content.trim();
message.editedAt = new Date();
// Broadcast update
io.to(roomId).emit('message:updated', message);
socket.ok({ messageId }, 'Message updated successfully');
} catch (error) {
socket.error(error, 'Failed to edit message');
}
});
// Delete message
socket.on('message:delete', (data) => {
try {
const { messageId, roomId } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
const messageIndex = room.messages.findIndex((m) => m.id === messageId);
if (messageIndex === -1) {
return socket.notFound({}, 'Message not found');
}
const message = room.messages[messageIndex];
if (message.userId !== user.id) {
return socket.forbidden({}, 'Can only delete your own messages');
}
// Remove message
room.messages.splice(messageIndex, 1);
// Broadcast deletion
io.to(roomId).emit('message:deleted', { messageId, roomId });
socket.ok({ messageId }, 'Message deleted successfully');
} catch (error) {
socket.error(error, 'Failed to delete message');
}
});
// Typing indicators
socket.on('typing:start', (data) => {
const { roomId } = data;
const user = userManager.getUser(socket.id);
if (user && roomId) {
socket.to(roomId).emit('typing:user_started', {
userId: user.id,
username: user.username,
roomId,
});
}
});
socket.on('typing:stop', (data) => {
const { roomId } = data;
const user = userManager.getUser(socket.id);
if (user && roomId) {
socket.to(roomId).emit('typing:user_stopped', {
userId: user.id,
username: user.username,
roomId,
});
}
});
});
}
module.exports = handleMessages;Room Management Events
javascript
// handlers/roomHandler.js
const { userManager } = require('../models/User');
const { roomManager } = require('../models/Room');
function handleRooms(io) {
io.on('connection', (socket) => {
// Join room
socket.on('room:join', (data) => {
try {
const { roomId } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
// Add user to room
const result = room.addMember(user.id);
if (result.error) {
return socket.badRequest({}, result.error);
}
// Join socket room
socket.join(roomId);
// Notify room members
socket.to(roomId).emit('room:user_joined', {
user: user.toJSON(),
roomId,
message: `${user.username} joined ${room.name}`,
});
// Send room data to user
socket.ok(
{
room: room.toJSON(),
recentMessages: room.getRecentMessages(20),
members: room.members.size,
},
`Joined ${room.name}`,
);
} catch (error) {
socket.error(error, 'Failed to join room');
}
});
// Leave room
socket.on('room:leave', (data) => {
try {
const { roomId } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
// Remove user from room
room.removeMember(user.id);
socket.leave(roomId);
// Notify room members
socket.to(roomId).emit('room:user_left', {
user: user.toJSON(),
roomId,
message: `${user.username} left ${room.name}`,
});
socket.ok({ roomId }, `Left ${room.name}`);
} catch (error) {
socket.error(error, 'Failed to leave room');
}
});
// Create room
socket.on('room:create', (data) => {
try {
const { name, isPrivate = false } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
if (!name || name.trim().length < 3) {
return socket.badRequest(
{ name: 'Room name must be at least 3 characters' },
'Invalid room name',
);
}
const roomId = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
const result = roomManager.createRoom(roomId, name.trim(), user.id);
if (result.error) {
return socket.conflict({}, result.error);
}
const room = result.room;
room.isPrivate = isPrivate;
// Creator automatically joins
room.addMember(user.id);
socket.join(roomId);
// Broadcast new room to all users
if (!isPrivate) {
io.emit('room:created', room.toJSON());
}
socket.created(room.toJSON(), 'Room created successfully');
} catch (error) {
socket.error(error, 'Failed to create room');
}
});
// Get room list
socket.on('room:list', () => {
try {
const rooms = roomManager.getAllRooms();
socket.ok(rooms, 'Rooms retrieved successfully');
} catch (error) {
socket.error(error, 'Failed to get room list');
}
});
// Get room messages
socket.on('room:messages', (data) => {
try {
const { roomId, limit = 50 } = data;
const user = userManager.getUser(socket.id);
if (!user) {
return socket.unauthorized({}, 'User not authenticated');
}
const room = roomManager.getRoom(roomId);
if (!room) {
return socket.notFound({}, 'Room not found');
}
if (!room.members.has(user.id)) {
return socket.forbidden({}, 'Not a member of this room');
}
const messages = room.getRecentMessages(Math.min(limit, 100));
socket.ok(
{
roomId,
messages,
total: room.messages.length,
},
'Messages retrieved successfully',
);
} catch (error) {
socket.error(error, 'Failed to get messages');
}
});
});
}
module.exports = handleRooms;Client Integration
JavaScript Client
javascript
// client/chat.js
class ChatClient {
constructor(serverUrl) {
this.socket = io(serverUrl);
this.currentUser = null;
this.currentRoom = null;
this.setupEventListeners();
}
setupEventListeners() {
// Connection events
this.socket.on('connect', () => {
console.log('Connected to server');
});
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
});
// Response events
this.socket.on('response', (response) => {
this.handleResponse(response);
});
// Message events
this.socket.on('message:received', (message) => {
this.displayMessage(message);
});
// User events
this.socket.on('user:joined', (data) => {
this.displayUserJoined(data);
});
this.socket.on('user:left', (data) => {
this.displayUserLeft(data);
});
// Typing events
this.socket.on('typing:user_started', (data) => {
this.showTypingIndicator(data);
});
this.socket.on('typing:user_stopped', (data) => {
this.hideTypingIndicator(data);
});
}
joinChat(username) {
this.socket.emit('user:join', { username });
}
sendMessage(content, roomId = this.currentRoom) {
if (!roomId) {
console.error('No room selected');
return;
}
this.socket.emit('message:send', {
roomId,
content,
type: 'text',
});
}
joinRoom(roomId) {
this.socket.emit('room:join', { roomId });
}
startTyping(roomId = this.currentRoom) {
if (roomId) {
this.socket.emit('typing:start', { roomId });
}
}
stopTyping(roomId = this.currentRoom) {
if (roomId) {
this.socket.emit('typing:stop', { roomId });
}
}
handleResponse(response) {
if (response.success) {
console.log('Success:', response.message);
} else {
console.error('Error:', response.message, response.error);
}
}
displayMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
messageElement.innerHTML = `
<span class="username">${message.username}</span>
<span class="timestamp">${new Date(message.timestamp).toLocaleTimeString()}</span>
<div class="content">${this.escapeHtml(message.content)}</div>
`;
document.getElementById('messages').appendChild(messageElement);
this.scrollToBottom();
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
scrollToBottom() {
const messages = document.getElementById('messages');
messages.scrollTop = messages.scrollHeight;
}
}
// Initialize chat
const chat = new ChatClient('http://localhost:3000');
// UI event handlers
document.getElementById('join-form').addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
if (username) {
chat.joinChat(username);
}
});
document.getElementById('message-form').addEventListener('submit', (e) => {
e.preventDefault();
const content = document.getElementById('message-input').value.trim();
if (content) {
chat.sendMessage(content);
document.getElementById('message-input').value = '';
}
});Complete Application
Main Server File
javascript
// index.js
const { app, io, httpServer } = require('./server');
const handleConnection = require('./handlers/connectionHandler');
const handleMessages = require('./handlers/messageHandler');
const handleRooms = require('./handlers/roomHandler');
// Set up Socket.IO event handlers
handleConnection(io);
handleMessages(io);
handleRooms(io);
// REST API endpoints
app.get('/api/health', (req, res) => {
res.ok(
{
status: 'healthy',
timestamp: new Date().toISOString(),
connections: io.engine.clientsCount,
},
'Chat server is healthy',
);
});
app.get('/api/stats', (req, res) => {
const { userManager, roomManager } = require('./models/User');
res.ok(
{
totalUsers: userManager.users.size,
totalRooms: roomManager.rooms.size,
connections: io.engine.clientsCount,
},
'Server statistics',
);
});
console.log('Chat application started successfully');This chat application demonstrates a complete real-time messaging system with user management, room functionality, message handling, and proper error responses using Response Handler.