Skip to content

Websocket Chat App

DodaTech 7 min read

title: "WebSocket Chat Application" description: "Build a real-time chat application with WebSocket including rooms, user presence, message history, typing indicators, and file sharing." weight: 26 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "websocket"]


A WebSocket chat application is the classic real-time application. This lesson walks through building a complete chat system with rooms, presence, typing indicators, message history, and file sharing.

## What You'll Learn

- Chat room management
- User presence and status
- Typing indicators
- Message history and persistence
- File sharing via WebSocket

## Why It Matters

Chat applications demonstrate all core WebSocket concepts in a single project. The patterns you learn apply to any real-time application: collaboration tools, customer support, and live streaming.

## Real-World Use

A customer support platform uses a WebSocket chat system where agents can handle multiple conversations simultaneously. Each conversation is a room, agents see typing indicators, and message history persists in the database.

## Flow Chart

```mermaid
flowchart TD
    A[Client Connects] --> B[Authentication]
    B --> C[Join Room]
    C --> D{Events}
    D --> E[Send Message]
    D --> F[Typing Indicator]
    D --> G[File Upload]
    D --> H[Leave Room]
    E --> I[Broadcast to Room]
    E --> J[Save to DB]
    F --> K[Broadcast to Room]
    G --> L[Upload to S3]
    G --> I
    H --> M[Update Presence]

Code Examples

Example 1: Chat Server with Rooms and History

const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');

const server = new WebSocket.Server({ port: 8080 });
const rooms = new Map(); // roomName -> Set<WebSocket>
const messageHistory = new Map(); // roomName -> Message[]
const userSessions = new Map(); // ws -> userInfo

server.on('connection', (ws) => {
  const sessionId = uuidv4();
  userSessions.set(ws, { id: sessionId, username: 'Anonymous', room: null });

  ws.on('message', (data) => {
    const message = JSON.parse(data);
    const user = userSessions.get(ws);

    switch (message.type) {
      case 'join':
        handleJoin(ws, user, message.room, message.username);
        break;
      case 'message':
        handleMessage(ws, user, message.text);
        break;
      case 'typing':
        handleTyping(ws, user, message.isTyping);
        break;
      case 'leave':
        handleLeave(ws, user);
        break;
    }
  });

  ws.on('close', () => {
    const user = userSessions.get(ws);
    if (user?.room) {
      handleLeave(ws, user);
    }
    userSessions.delete(ws);
  });
});

function handleJoin(ws, user, roomName, username) {
  // Leave previous room
  if (user.room) {
    handleLeave(ws, user);
  }

  user.username = username || `User-${user.id.slice(0, 4)}`;
  user.room = roomName;

  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
    messageHistory.set(roomName, []);
  }

  rooms.get(roomName).add(ws);
  
  // Send room info to joining user
  ws.send(JSON.stringify({
    type: 'room-joined',
    room: roomName,
    history: messageHistory.get(roomName).slice(-50),
    users: getRoomUsers(roomName),
  }));

  // Notify room about new user
  broadcastToRoom(roomName, {
    type: 'user-joined',
    userId: user.id,
    username: user.username,
    users: getRoomUsers(roomName),
  }, ws);
}

function handleMessage(ws, user, text) {
  if (!user.room || !text?.trim()) return;

  const message = {
    id: uuidv4(),
    type: 'message',
    userId: user.id,
    username: user.username,
    text: text.trim(),
    timestamp: Date.now(),
  };

  // Save to history
  const history = messageHistory.get(user.room);
  history.push(message);
  if (history.length > 200) history.shift();

  broadcastToRoom(user.room, message);
}

function handleTyping(ws, user, isTyping) {
  if (!user.room) return;
  broadcastToRoom(user.room, {
    type: 'typing',
    userId: user.id,
    username: user.username,
    isTyping,
  }, ws);
}

function handleLeave(ws, user) {
  if (!user.room) return;

  rooms.get(user.room)?.delete(ws);
  if (rooms.get(user.room)?.size === 0) {
    rooms.delete(user.room);
  }

  broadcastToRoom(user.room, {
    type: 'user-left',
    userId: user.id,
    username: user.username,
    users: getRoomUsers(user.room),
  });

  user.room = null;
}

function broadcastToRoom(roomName, message, exclude = null) {
  const room = rooms.get(roomName);
  if (!room) return;

  const data = JSON.stringify(message);
  room.forEach((client) => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

function getRoomUsers(roomName) {
  const room = rooms.get(roomName);
  if (!room) return [];

  return Array.from(room).map((ws) => {
    const user = userSessions.get(ws);
    return { id: user.id, username: user.username };
  });
}

Expected output: Full chat server with room management, message history, user presence, and typing indicators.

Example 2: Chat Client with UI Integration

class ChatClient {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.room = null;
    this.username = null;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.serverUrl);
    
    this.ws.onopen = () => {
      console.log('Connected to chat server');
      if (this.room) {
        this.joinRoom(this.room, this.username);
      }
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };

    this.ws.onclose = () => {
      console.log('Disconnected, reconnecting...');
      setTimeout(() => this.connect(), 2000);
    };
  }

  joinRoom(room, username) {
    this.room = room;
    this.username = username;
    this.send({ type: 'join', room, username });
  }

  sendMessage(text) {
    this.send({ type: 'message', text });
  }

  setTyping(isTyping) {
    this.send({ type: 'typing', isTyping });
  }

  send(data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }

  handleMessage(message) {
    switch (message.type) {
      case 'room-joined':
        this.renderHistory(message.history);
        this.renderUserList(message.users);
        break;
      case 'message':
        this.renderMessage(message);
        break;
      case 'typing':
        this.renderTypingIndicator(message);
        break;
      case 'user-joined':
      case 'user-left':
        this.renderNotification(message);
        this.renderUserList(message.users);
        break;
    }
  }

  renderMessage(msg) {
    const div = document.createElement('div');
    div.className = 'message';
    div.innerHTML = `
      <span class="author">${escapeHtml(msg.username)}</span>
      <span class="time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
      <p>${escapeHtml(msg.text)}</p>
    `;
    document.getElementById('messages').appendChild(div);
  }

  renderTypingIndicator(data) {
    const indicator = document.getElementById('typing-indicator');
    if (data.isTyping) {
      indicator.textContent = `${data.username} is typing...`;
    } else {
      indicator.textContent = '';
    }
  }
}

// UI event handlers
const chat = new ChatClient('wss://chat.example.com/ws');

document.getElementById('join-btn').onclick = () => {
  const room = document.getElementById('room-input').value;
  const username = document.getElementById('username-input').value;
  chat.joinRoom(room, username);
};

document.getElementById('send-btn').onclick = () => {
  const input = document.getElementById('message-input');
  if (input.value.trim()) {
    chat.sendMessage(input.value);
    input.value = '';
  }
};

document.getElementById('message-input').oninput = () => {
  chat.setTyping(input.value.length > 0);
};

Expected output: Chat client connects to server, joins rooms, sends/receives messages, and shows typing indicators with proper HTML escaping.

Example 3: File Sharing via WebSocket

// Server-side file handling
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');

const server = new WebSocket.Server({ port: 8080 });
const UPLOAD_DIR = './uploads';
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

if (!fs.existsSync(UPLOAD_DIR)) {
  fs.mkdirSync(UPLOAD_DIR);
}

server.on('connection', (ws) => {
  let fileBuffer = null;
  let fileInfo = null;

  ws.on('message', (data) => {
    // Check if this is a binary file chunk
    if (Buffer.isBuffer(data) && fileInfo) {
      fileBuffer = Buffer.concat([fileBuffer, data]);
      
      // Check file size during transfer
      if (fileBuffer.length > MAX_FILE_SIZE) {
        ws.send(JSON.stringify({
          type: 'file-error',
          message: 'File exceeds maximum size',
        }));
        fileBuffer = null;
        fileInfo = null;
        return;
      }
      return;
    }

    const message = JSON.parse(data);

    switch (message.type) {
      case 'file-start':
        fileInfo = {
          id: uuidv4(),
          name: message.name,
          size: message.size,
          mimeType: message.mimeType,
          room: message.room,
        };
        fileBuffer = Buffer.alloc(0);
        ws.send(JSON.stringify({
          type: 'file-ready',
          fileId: fileInfo.id,
        }));
        break;

      case 'file-end':
        if (fileBuffer && fileInfo) {
          const ext = path.extname(fileInfo.name);
          const filename = `${fileInfo.id}${ext}`;
          const filepath = path.join(UPLOAD_DIR, filename);
          
          fs.writeFileSync(filepath, fileBuffer);
          
          // Broadcast file info to room
          broadcastToRoom(fileInfo.room, {
            type: 'file',
            fileId: fileInfo.id,
            name: fileInfo.name,
            size: fileBuffer.length,
            mimeType: fileInfo.mimeType,
            url: `/download/${filename}`,
            username: message.username,
            timestamp: Date.now(),
          });

          fileBuffer = null;
          fileInfo = null;
        }
        break;

      case 'file-cancel':
        fileBuffer = null;
        fileInfo = null;
        break;
    }
  });
});

Expected output: File upload over WebSocket with chunked binary transfer, size validation, and broadcast notification to the room.

Common Mistakes

Mistake Explanation
Not persisting message history Without persistence, new users cannot see previous messages
Sending raw user input to UI Always escape HTML entities to prevent XSS attacks
Not handling reconnection gracefully Users lose chat state on disconnect without proper reconnection handling
Broadcasting typing to all users Only broadcast typing indicators to the relevant room
Not cleaning up disconnected users Remove users from rooms on disconnect to maintain accurate presence

Practice Questions

  1. How do you handle message persistence in a WebSocket chat?
  2. How do typing indicators work without creating excessive traffic?
  3. How do you manage user presence across multiple rooms?
  4. How do you implement file sharing over WebSocket?
  5. How do you handle chat history for new users joining a room?

Challenge

Extend the chat application with message reactions, message editing, read receipts, private messaging (1:1 chats), and search functionality across all messages. Implement message persistence with a database.

FAQ

How do I implement message persistence?

Store messages in a database (PostgreSQL, MongoDB) on the server side. Load recent message history when a user joins a room.

How do I handle 1:1 private messages?

Create a unique room name based on the two user IDs (e.g., dm:userA:userB). Only those two users join the room.

How do typing indicators work?

Clients send typing events on keystroke with debouncing. The server broadcasts to the room. Clients show a typing indicator that auto-hides after 2 seconds of no typing events.

How do I implement message reactions?

Send reaction events with message ID and reaction type. Store reactions in the database and include them when loading message history.

What is the best way to moderate chat messages?

Implement a moderation layer that checks messages against banned words, rate limits, and user reputation before broadcasting.

How do I scale a chat application to millions of users?

Use a message broker (Redis pub/sub, Kafka) to sync messages across server instances. Use rooms to scope broadcasts to relevant users.

Mini Project

Build a complete chat application with WebSocket including room management, user presence, message history (persisted to database), file sharing, typing indicators, and message reactions. Deploy with Docker and include a simple web UI.

What's Next

Build a real-time dashboard with WebSocket

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro