Guides

Chat Application

A complete walkthrough of the IOServer chat application example.

Chat Application

The chat application at examples/chat-app/ demonstrates a real-world IOServer backend with all five component types working together.

What it does

  • Username login (no password, unique per session)
  • Default general room on join
  • Room creation and switching
  • Message history per room
  • Typing indicators
  • Real-time user lists
  • Statistics API
  • Responsive web UI served from the controller (HTML inline)

Structure

examples/chat-app/
├── app.ts                          # Entry point
├── services/
│   └── ChatService.ts              # WebSocket events
├── controllers/
│   ├── ChatController.ts           # Serves HTML UI
│   └── ApiController.ts            # REST API
├── managers/
│   └── StatsManager.ts             # Shared statistics
├── watchers/
│   └── ChatWatcher.ts              # Background tasks
└── routes/
    ├── chat.json                   # ChatController routes
    └── api.json                    # ApiController routes

Entry point (app.ts)

import { IOServer } from "../../src";
import { ChatService } from "./services/ChatService";
import { ChatController } from "./controllers/ChatController";
import { ApiController } from "./controllers/ApiController";
import { StatsManager } from "./managers/StatsManager";
import { ChatWatcher } from "./watchers/ChatWatcher";

const server = new IOServer({
  host: "localhost",
  port: 3000,
  verbose: "DEBUG",
  cors: { origin: ["http://localhost:3000"], methods: ["GET", "POST"] },
  mode: ["websocket", "polling"],
  routes: "./examples/chat-app/routes",
});

// 1. Managers first — available in appHandle before anything else
server.addManager({ name: "statsManager", manager: StatsManager });

// 2. Watchers — watch() called at start()
server.addWatcher({ name: "chatWatcher", watcher: ChatWatcher });

// 3. Services — WebSocket namespace /chat
server.addService({ name: "chat", service: ChatService });

// 4. Controllers — HTTP routes registered now
server.addController({ name: "chat", controller: ChatController, prefix: "" });
server.addController({ name: "api",  controller: ApiController  });

await server.start();

Why managers first? StatsManager is accessed by ChatService, ChatWatcher, and ApiController. It must be on appHandle before those components are instantiated.

Routes

routes/chat.jsonChatController

[
  { "method": "GET", "url": "/",       "handler": "getIndex"  },
  { "method": "GET", "url": "/health", "handler": "getHealth" }
]

With prefix: "":

  • GET /getIndex (serves full HTML chat UI)
  • GET /healthgetHealth

routes/api.jsonApiController

[
  { "method": "GET", "url": "/status", "handler": "getStatus" },
  { "method": "GET", "url": "/stats",  "handler": "getStats"  }
]

With default prefix (/api):

  • GET /api/statusgetStatus
  • GET /api/statsgetStats (returns live stats from StatsManager)

StatsManager

Singleton tracking usage numbers. The start() hook is used to log its initialization:

export class StatsManager extends BaseManager {
  private stats = { totalUsers: 0, activeRooms: 0, messagesCount: 0, peakConcurrentUsers: 0 };
  private startTime = new Date();

  incrementUsers(): void { /* ... updates totalUsers and peak */ }
  decrementUsers(): void { /* ... */ }
  incrementMessages(): void { /* ... */ }
  setActiveRooms(count: number): void { /* ... */ }

  getStats(): typeof this.stats & { uptime: string } {
    const diffMs = Date.now() - this.startTime.getTime();
    // format uptime ...
    return { ...this.stats, uptime };
  }
}

ApiController.getStats() calls this.appHandle.statsManager.getStats() to expose this over HTTP.

ChatService highlights

All user/room state lives inside the service instance (in-memory Maps):

private users: Map<string, User> = new Map();
private rooms: Map<string, Set<string>> = new Map();
private messageHistory: Map<string, Message[]> = new Map();
private usernames: Set<string> = new Set();

login event

  1. Validates username (non-empty, unique)
  2. Creates a User record keyed by socket.id
  3. Joins the general Socket.IO room
  4. Broadcasts a system message to the room
  5. Calls back with current room users and recent messages
async login(socket: any, data: { username: string }, callback?: Function): Promise<void> {
  // validate → create user → socket.join("general") → broadcast → callback
}

send_message event

  1. Looks up user by socket.id
  2. Creates a message object
  3. Broadcasts to room via appHandle.send({ namespace: "chat", room, event: "new_message", data: message })
  4. Increments message count on statsManager

join_room event

  1. Leaves current Socket.IO room (socket.leave)
  2. Creates room if new
  3. Joins new Socket.IO room (socket.join)
  4. Sends system messages to both old and new room

disconnect event

Socket.IO fires disconnect automatically. Since disconnect does not start with _, IOServer registers it as a socket event handler:

async disconnect(socket: any, data: any): Promise<void> {
  const user = this.users.get(socket.id);
  if (!user) return;

  // Remove from room and user maps
  this.rooms.get(user.room)?.delete(socket.id);
  this.users.delete(socket.id);
  this.usernames.delete(user.username.toLowerCase());

  // Notify room
  socket.to(user.room).emit("user_left", { username: user.username });

  this.appHandle.statsManager?.decrementUsers();
  this.appHandle.log(6, `${user.username} disconnected`);
}

ChatWatcher

Runs three periodic checks in the background:

async watch(): Promise<void> {
  // Cleanup every 30 minutes
  this.intervals.push(setInterval(() => this.cleanupOldMessages(), 30 * 60_000));

  // Log stats every 5 minutes
  this.intervals.push(setInterval(() => this.logStats(), 5 * 60_000));

  // Memory health every minute
  this.intervals.push(setInterval(() => this.monitorHealth(), 60_000));
}

logStats() reads from appHandle.statsManager:

private logStats(): void {
  const stats = this.appHandle.statsManager?.getStats();
  if (stats) this.appHandle.log(6, `Users: ${stats.totalUsers}, Messages: ${stats.messagesCount}`);
}

Running the example

# From the IOServer root
pnpm run dev:chat
# or
npx ts-node examples/chat-app/app.ts

Open http://localhost:3000 to see the chat UI, or:

curl http://localhost:3000/api/stats
Copyright © 2026