Websocket Chat App
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
- How do you handle message persistence in a WebSocket chat?
- How do typing indicators work without creating excessive traffic?
- How do you manage user presence across multiple rooms?
- How do you implement file sharing over WebSocket?
- 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
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