Chat Application Example
This comprehensive example demonstrates how to build a real-time chat application using IOServer. The chat app showcases all core features including real-time messaging, user management, room functionality, and HTTP API endpoints.
Overview
The chat application demonstrates:
Real-time messaging with Socket.IO
User authentication and management
Room-based conversations
Typing indicators
Connection management
RESTful API for statistics
Message history
User presence tracking
Architecture
The chat application follows IOServer’s modular architecture:
examples/chat-app/
├── app.ts # Main application entry point
├── public/ # Static web files
│ ├── index.html # Chat interface
│ ├── style.css # Styling
│ └── script.js # Client-side logic
├── routes/ # HTTP route definitions
│ └── api.json # API endpoints
├── services/ # Real-time services
│ └── ChatService.ts # Chat functionality
├── controllers/ # HTTP controllers
│ └── ApiController.ts # API endpoints
└── managers/ # Shared logic
└── ChatManager.ts # Chat data management
Implementation Details
1. Main Application (app.ts)
import { IOServer } from '../../src';
import { ChatService } from './services/ChatService';
import { ApiController } from './controllers/ApiController';
import { ChatManager } from './managers/ChatManager';
const server = new IOServer({
host: 'localhost',
port: 8080,
verbose: 'INFO',
routes: './examples/chat-app/routes',
cors: {
origin: ['http://localhost:8080'],
methods: ['GET', 'POST'],
credentials: false,
},
});
// Register manager for shared data
server.addManager({
name: 'chat',
manager: ChatManager,
});
// Register real-time chat service
server.addService({
name: 'chat',
service: ChatService,
});
// Register HTTP API controller
server.addController({
name: 'api',
controller: ApiController,
});
// Serve static files
server.getApp().register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/',
});
await server.start();
console.log('🚀 Chat application running at http://localhost:8080');
2. Chat Manager (managers/ChatManager.ts)
The Chat Manager handles shared data and business logic:
import { BaseManager } from '../../../src';
interface User {
id: string;
username: string;
room: string;
socketId: string;
joinedAt: Date;
}
interface Message {
id: string;
user: string;
message: string;
room: string;
timestamp: Date;
}
interface Room {
name: string;
users: User[];
messageCount: number;
createdAt: Date;
}
export class ChatManager extends BaseManager {
private users: Map<string, User> = new Map();
private rooms: Map<string, Room> = new Map();
private messages: Message[] = [];
private typing: Map<string, Set<string>> = new Map();
// User Management
addUser(user: User): void {
this.users.set(user.socketId, user);
// Add user to room
if (!this.rooms.has(user.room)) {
this.rooms.set(user.room, {
name: user.room,
users: [],
messageCount: 0,
createdAt: new Date(),
});
}
const room = this.rooms.get(user.room)!;
room.users.push(user);
this.appHandle.log(6, `User ${user.username} joined room ${user.room}`);
}
removeUser(socketId: string): User | null {
const user = this.users.get(socketId);
if (!user) return null;
this.users.delete(socketId);
// Remove from room
const room = this.rooms.get(user.room);
if (room) {
room.users = room.users.filter(u => u.socketId !== socketId);
// Remove empty rooms
if (room.users.length === 0) {
this.rooms.delete(user.room);
}
}
// Remove from typing indicators
this.stopTyping(socketId);
this.appHandle.log(6, `User ${user.username} left room ${user.room}`);
return user;
}
// Message Management
addMessage(user: string, message: string, room: string): Message {
const newMessage: Message = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
user,
message,
room,
timestamp: new Date(),
};
this.messages.push(newMessage);
// Update room message count
const roomData = this.rooms.get(room);
if (roomData) {
roomData.messageCount++;
}
// Keep only last 1000 messages to prevent memory issues
if (this.messages.length > 1000) {
this.messages = this.messages.slice(-1000);
}
return newMessage;
}
getRecentMessages(room: string, limit: number = 50): Message[] {
return this.messages.filter(msg => msg.room === room).slice(-limit);
}
// Typing Indicators
startTyping(room: string, username: string): void {
if (!this.typing.has(room)) {
this.typing.set(room, new Set());
}
this.typing.get(room)!.add(username);
}
stopTyping(socketId: string): void {
const user = this.users.get(socketId);
if (!user) return;
const roomTyping = this.typing.get(user.room);
if (roomTyping) {
roomTyping.delete(user.username);
if (roomTyping.size === 0) {
this.typing.delete(user.room);
}
}
}
getTypingUsers(room: string): string[] {
return Array.from(this.typing.get(room) || []);
}
// Statistics
getStats() {
return {
totalUsers: this.users.size,
totalRooms: this.rooms.size,
totalMessages: this.messages.length,
rooms: Array.from(this.rooms.values()).map(room => ({
name: room.name,
userCount: room.users.length,
messageCount: room.messageCount,
users: room.users.map(u => u.username),
})),
};
}
getRoomUsers(room: string): User[] {
const roomData = this.rooms.get(room);
return roomData ? roomData.users : [];
}
getUserBySocket(socketId: string): User | undefined {
return this.users.get(socketId);
}
}
3. Chat Service (services/ChatService.ts)
The Chat Service handles all real-time interactions:
import { BaseService } from '../../../src';
export class ChatService extends BaseService {
// User joins a chat room
async joinRoom(
socket: any,
data: { username: string; room: string },
callback?: Function
) {
try {
const { username, room } = data;
if (!username || !room) {
throw new Error('Username and room are required');
}
// Add user to chat manager
const user = {
id: `user_${Date.now()}`,
username,
room,
socketId: socket.id,
joinedAt: new Date(),
};
this.appHandle.chat.addUser(user);
// Join Socket.IO room
await socket.join(room);
// Get room users and recent messages
const roomUsers = this.appHandle.chat.getRoomUsers(room);
const recentMessages = this.appHandle.chat.getRecentMessages(room);
// Notify user of successful join
socket.emit('room_joined', {
user,
users: roomUsers.map(u => ({
username: u.username,
joinedAt: u.joinedAt,
})),
messages: recentMessages,
});
// Notify other users in room
socket.broadcast.to(room).emit('user_joined', {
user: { username, joinedAt: user.joinedAt },
userCount: roomUsers.length,
});
if (callback) {
callback({ status: 'success', message: 'Joined room successfully' });
}
} catch (error) {
this.appHandle.log(3, `Error in joinRoom: ${error}`);
if (callback) {
callback({ status: 'error', message: error.message });
}
}
}
// User sends a message
async sendMessage(
socket: any,
data: { message: string },
callback?: Function
) {
try {
const user = this.appHandle.chat.getUserBySocket(socket.id);
if (!user) {
throw new Error('User not found');
}
const { message } = data;
if (!message || message.trim().length === 0) {
throw new Error('Message cannot be empty');
}
// Add message to chat manager
const newMessage = this.appHandle.chat.addMessage(
user.username,
message.trim(),
user.room
);
// Stop typing indicator for this user
this.appHandle.chat.stopTyping(socket.id);
// Broadcast message to room
socket.broadcast.to(user.room).emit('new_message', newMessage);
// Send confirmation to sender
socket.emit('message_sent', newMessage);
// Update typing indicators
const typingUsers = this.appHandle.chat.getTypingUsers(user.room);
socket.broadcast.to(user.room).emit('typing_update', typingUsers);
if (callback) {
callback({
status: 'success',
messageId: newMessage.id,
timestamp: newMessage.timestamp,
});
}
} catch (error) {
this.appHandle.log(3, `Error in sendMessage: ${error}`);
if (callback) {
callback({ status: 'error', message: error.message });
}
}
}
// Handle typing indicators
async startTyping(socket: any, data: any, callback?: Function) {
try {
const user = this.appHandle.chat.getUserBySocket(socket.id);
if (!user) return;
this.appHandle.chat.startTyping(user.room, user.username);
const typingUsers = this.appHandle.chat.getTypingUsers(user.room);
socket.broadcast.to(user.room).emit('typing_update', typingUsers);
if (callback) callback({ status: 'success' });
} catch (error) {
if (callback) callback({ status: 'error', message: error.message });
}
}
async stopTyping(socket: any, data: any, callback?: Function) {
try {
const user = this.appHandle.chat.getUserBySocket(socket.id);
if (!user) return;
this.appHandle.chat.stopTyping(socket.id);
const typingUsers = this.appHandle.chat.getTypingUsers(user.room);
socket.broadcast.to(user.room).emit('typing_update', typingUsers);
if (callback) callback({ status: 'success' });
} catch (error) {
if (callback) callback({ status: 'error', message: error.message });
}
}
// Handle user disconnection
async handleDisconnect(socket: any) {
const user = this.appHandle.chat.removeUser(socket.id);
if (user) {
// Notify room about user leaving
socket.broadcast.to(user.room).emit('user_left', {
user: { username: user.username },
userCount: this.appHandle.chat.getRoomUsers(user.room).length,
});
// Update typing indicators
const typingUsers = this.appHandle.chat.getTypingUsers(user.room);
socket.broadcast.to(user.room).emit('typing_update', typingUsers);
}
}
// Get room information
async getRoomInfo(socket: any, data: any, callback?: Function) {
try {
const user = this.appHandle.chat.getUserBySocket(socket.id);
if (!user) {
throw new Error('User not found');
}
const roomUsers = this.appHandle.chat.getRoomUsers(user.room);
const typingUsers = this.appHandle.chat.getTypingUsers(user.room);
const roomInfo = {
room: user.room,
userCount: roomUsers.length,
users: roomUsers.map(u => ({
username: u.username,
joinedAt: u.joinedAt,
})),
typingUsers,
};
if (callback) callback({ status: 'success', roomInfo });
} catch (error) {
if (callback) callback({ status: 'error', message: error.message });
}
}
}
4. API Controller (controllers/ApiController.ts)
The API Controller provides HTTP endpoints for statistics and management:
import { BaseController } from '../../../src';
export class ApiController extends BaseController {
// Get server status
async getStatus(request: any, reply: any) {
const status = {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: '2.0.0',
};
reply.send(status);
}
// Get chat statistics
async getStats(request: any, reply: any) {
try {
const stats = this.appHandle.chat.getStats();
const response = {
...stats,
serverInfo: {
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
timestamp: new Date().toISOString(),
},
};
reply.send(response);
} catch (error) {
this.appHandle.log(3, `Error getting stats: ${error}`);
reply.status(500).send({
statusCode: 500,
error: 'Internal Server Error',
message: 'Unable to retrieve statistics',
});
}
}
// Get room details
async getRoomDetails(request: any, reply: any) {
try {
const { room } = request.params;
if (!room) {
return reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
message: 'Room parameter is required',
});
}
const roomUsers = this.appHandle.chat.getRoomUsers(room);
const recentMessages = this.appHandle.chat.getRecentMessages(room, 20);
const roomDetails = {
room,
userCount: roomUsers.length,
users: roomUsers.map(u => ({
username: u.username,
joinedAt: u.joinedAt,
})),
recentMessages: recentMessages.map(m => ({
id: m.id,
user: m.user,
message: m.message,
timestamp: m.timestamp,
})),
};
reply.send(roomDetails);
} catch (error) {
this.appHandle.log(3, `Error getting room details: ${error}`);
reply.status(500).send({
statusCode: 500,
error: 'Internal Server Error',
message: 'Unable to retrieve room details',
});
}
}
}
5. Route Configuration (routes/api.json)
[
{
"method": "GET",
"url": "/status",
"handler": "getStatus"
},
{
"method": "GET",
"url": "/stats",
"handler": "getStats"
},
{
"method": "GET",
"url": "/rooms/:room",
"handler": "getRoomDetails"
}
]
Client-Side Implementation
HTML Interface (public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IOServer Chat Example</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="app">
<!-- Login Form -->
<div id="login-form" class="form-container">
<h2>Join Chat</h2>
<form id="join-form">
<input
type="text"
id="username"
placeholder="Your username"
required
/>
<input type="text" id="room" placeholder="Room name" required />
<button type="submit">Join Chat</button>
</form>
</div>
<!-- Chat Interface -->
<div id="chat-interface" class="hidden">
<div class="chat-header">
<h2 id="room-title">Room:</h2>
<div class="user-info">
<span id="user-name"></span>
<span id="user-count">0 users</span>
</div>
</div>
<div class="chat-container">
<div class="sidebar">
<h3>Users Online</h3>
<ul id="user-list"></ul>
</div>
<div class="chat-main">
<div id="messages" class="messages"></div>
<div id="typing-indicator" class="typing-indicator"></div>
<form id="message-form" class="message-form">
<input
type="text"
id="message-input"
placeholder="Type a message..."
required
/>
<button type="submit">Send</button>
</form>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="script.js"></script>
</body>
</html>
JavaScript Client (public/script.js)
class ChatClient {
constructor() {
this.socket = null;
this.username = '';
this.room = '';
this.isTyping = false;
this.typingTimeout = null;
this.initializeElements();
this.bindEvents();
}
initializeElements() {
this.loginForm = document.getElementById('login-form');
this.chatInterface = document.getElementById('chat-interface');
this.joinForm = document.getElementById('join-form');
this.messageForm = document.getElementById('message-form');
this.messageInput = document.getElementById('message-input');
this.messagesContainer = document.getElementById('messages');
this.userList = document.getElementById('user-list');
this.typingIndicator = document.getElementById('typing-indicator');
this.roomTitle = document.getElementById('room-title');
this.userName = document.getElementById('user-name');
this.userCount = document.getElementById('user-count');
}
bindEvents() {
this.joinForm.addEventListener('submit', e => this.handleJoin(e));
this.messageForm.addEventListener('submit', e => this.handleSendMessage(e));
this.messageInput.addEventListener('input', () => this.handleTyping());
}
handleJoin(e) {
e.preventDefault();
this.username = document.getElementById('username').value.trim();
this.room = document.getElementById('room').value.trim();
if (this.username && this.room) {
this.connectToChat();
}
}
connectToChat() {
this.socket = io('/chat');
this.socket.on('connect', () => {
console.log('Connected to chat server');
this.joinRoom();
});
this.socket.on('room_joined', data => this.handleRoomJoined(data));
this.socket.on('user_joined', data => this.handleUserJoined(data));
this.socket.on('user_left', data => this.handleUserLeft(data));
this.socket.on('new_message', message => this.displayMessage(message));
this.socket.on('message_sent', message =>
this.displayMessage(message, true)
);
this.socket.on('typing_update', users => this.updateTypingIndicator(users));
this.socket.on('disconnect', () => {
console.log('Disconnected from chat server');
this.showError('Disconnected from server');
});
this.socket.on('error', error => {
console.error('Socket error:', error);
this.showError(error.message);
});
}
joinRoom() {
this.socket.emit(
'joinRoom',
{
username: this.username,
room: this.room,
},
response => {
if (response.status === 'error') {
this.showError(response.message);
}
}
);
}
handleRoomJoined(data) {
this.loginForm.classList.add('hidden');
this.chatInterface.classList.remove('hidden');
this.roomTitle.textContent = `Room: ${this.room}`;
this.userName.textContent = this.username;
this.updateUserList(data.users);
this.displayPreviousMessages(data.messages);
this.messageInput.focus();
}
handleUserJoined(data) {
this.userCount.textContent = `${data.userCount} users`;
this.displaySystemMessage(`${data.user.username} joined the room`);
}
handleUserLeft(data) {
this.userCount.textContent = `${data.userCount} users`;
this.displaySystemMessage(`${data.user.username} left the room`);
}
handleSendMessage(e) {
e.preventDefault();
const message = this.messageInput.value.trim();
if (message) {
this.socket.emit('sendMessage', { message }, response => {
if (response.status === 'error') {
this.showError(response.message);
}
});
this.messageInput.value = '';
this.stopTyping();
}
}
handleTyping() {
if (!this.isTyping) {
this.isTyping = true;
this.socket.emit('startTyping');
}
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => {
this.stopTyping();
}, 2000);
}
stopTyping() {
if (this.isTyping) {
this.isTyping = false;
this.socket.emit('stopTyping');
}
clearTimeout(this.typingTimeout);
}
displayMessage(message, isSent = false) {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
if (isSent) messageElement.classList.add('sent');
const time = new Date(message.timestamp).toLocaleTimeString();
messageElement.innerHTML = `
<div class="message-header">
<span class="username">${message.user}</span>
<span class="timestamp">${time}</span>
</div>
<div class="message-content">${this.escapeHtml(message.message)}</div>
`;
this.messagesContainer.appendChild(messageElement);
this.scrollToBottom();
}
displaySystemMessage(message) {
const messageElement = document.createElement('div');
messageElement.classList.add('message', 'system');
messageElement.innerHTML = `<div class="message-content">${message}</div>`;
this.messagesContainer.appendChild(messageElement);
this.scrollToBottom();
}
displayPreviousMessages(messages) {
messages.forEach(message => this.displayMessage(message));
}
updateUserList(users) {
this.userList.innerHTML = '';
users.forEach(user => {
const li = document.createElement('li');
li.textContent = user.username;
this.userList.appendChild(li);
});
this.userCount.textContent = `${users.length} users`;
}
updateTypingIndicator(typingUsers) {
if (typingUsers.length === 0) {
this.typingIndicator.textContent = '';
} else if (typingUsers.length === 1) {
this.typingIndicator.textContent = `${typingUsers[0]} is typing...`;
} else {
this.typingIndicator.textContent = `${typingUsers.join(
', '
)} are typing...`;
}
}
showError(message) {
const errorElement = document.createElement('div');
errorElement.classList.add('error-message');
errorElement.textContent = message;
document.body.appendChild(errorElement);
setTimeout(() => {
errorElement.remove();
}, 5000);
}
scrollToBottom() {
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize chat client when page loads
document.addEventListener('DOMContentLoaded', () => {
new ChatClient();
});
Features Demonstrated
1. Real-time Messaging
Instant message delivery using Socket.IO
Message persistence in memory
Message history loading
2. User Management
User registration with username and room
User presence tracking
Online user list display
3. Room System
Room-based conversations
Multiple rooms support
Room statistics
4. Typing Indicators
Real-time typing status
Auto-timeout for typing indicators
Multiple users typing display
5. Connection Management
Graceful connection handling
Automatic cleanup on disconnect
Reconnection support
6. HTTP API
Server status endpoint
Chat statistics endpoint
Room information endpoint
7. Error Handling
Comprehensive error catching
User-friendly error messages
Graceful degradation
Running the Example
Start the server:
cd examples/chat-app npm start # or pnpm dev:chat
Open the application: Visit
http://localhost:8080
in your browserTest the features:
Open multiple browser tabs/windows
Join different rooms
Send messages and see real-time updates
Test typing indicators
Check API endpoints at
/api/stats
Key Learning Points
This example demonstrates:
Modular Architecture: Clean separation of concerns
Real-time Communication: Bidirectional event-based communication
State Management: Shared state across components using managers
Error Handling: Comprehensive error management
Client-Server Interaction: Both real-time and HTTP communication
Production Patterns: Scalable patterns for real-world applications
The chat application serves as a comprehensive template for building real-time applications with IOServer.