WebSockets & Real-Time Apps

Building a Real-Time Chat Application

20 min Lesson 13 of 35

Building a Real-Time Chat Application

Building a real-time chat application is one of the most practical applications of WebSockets. In this lesson, we'll create a full-featured chat system with user presence, message broadcasting, typing indicators, and message history.

Chat Application Architecture

A complete chat application consists of several key components:

  • User Authentication: Verify users before allowing them to chat
  • Connection Management: Track online users and their status
  • Message Broadcasting: Distribute messages to all connected users
  • Typing Indicators: Show when users are typing
  • Message History: Store and retrieve past messages
  • User Presence: Display online/offline status

Server-Side Chat Setup

Let's start by setting up the server-side structure for our chat application:

// server.js - Chat Server const express = require('express'); const app = express(); const http = require('http').createServer(app); const io = require('socket.io')(http); // Store connected users const users = new Map(); // socket.id => user object // Store chat messages in memory (use database in production) const messages = []; // Authentication middleware io.use((socket, next) => { const username = socket.handshake.auth.username; if (!username) { return next(new Error('Username required')); } socket.username = username; next(); }); io.on('connection', (socket) => { console.log(`User ${socket.username} connected`); // Add user to online users users.set(socket.id, { id: socket.id, username: socket.username, joinedAt: Date.now() }); // Send user their socket ID socket.emit('connected', { id: socket.id, username: socket.username }); // Notify all users that someone joined socket.broadcast.emit('userJoined', { id: socket.id, username: socket.username, timestamp: Date.now() }); // Send current online users list socket.emit('onlineUsers', Array.from(users.values())); // Send message history socket.emit('messageHistory', messages); // Handle new messages socket.on('sendMessage', (message) => { const messageData = { id: Date.now() + Math.random(), userId: socket.id, username: socket.username, message: message, timestamp: Date.now() }; // Store message messages.push(messageData); // Broadcast to all users including sender io.emit('newMessage', messageData); }); // Handle typing indicator socket.on('typing', () => { socket.broadcast.emit('userTyping', { username: socket.username, userId: socket.id }); }); socket.on('stopTyping', () => { socket.broadcast.emit('userStoppedTyping', { userId: socket.id }); }); // Handle disconnection socket.on('disconnect', () => { console.log(`User ${socket.username} disconnected`); // Remove user from online list users.delete(socket.id); // Notify others that user left socket.broadcast.emit('userLeft', { id: socket.id, username: socket.username, timestamp: Date.now() }); }); }); const PORT = 3000; http.listen(PORT, () => { console.log(`Chat server running on port ${PORT}`); });
Note: This example stores messages in memory. In production, use a database like MongoDB or PostgreSQL to persist chat history.

Client-Side Chat Interface

Now let's create the HTML structure for the chat interface:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Real-Time Chat</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background: #f0f2f5; } .chat-container { max-width: 800px; margin: 20px auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: flex; height: 600px; } .users-panel { width: 200px; border-right: 1px solid #e0e0e0; padding: 20px; } .users-panel h3 { margin-bottom: 15px; color: #333; } .user-item { padding: 8px; margin: 5px 0; background: #f5f5f5; border-radius: 4px; font-size: 14px; } .chat-panel { flex: 1; display: flex; flex-direction: column; } .chat-header { padding: 20px; border-bottom: 1px solid #e0e0e0; background: #0084ff; color: white; } .messages { flex: 1; padding: 20px; overflow-y: auto; } .message { margin: 10px 0; padding: 10px; background: #f1f3f4; border-radius: 8px; max-width: 70%; } .message.own { margin-left: auto; background: #0084ff; color: white; } .message-header { font-size: 12px; font-weight: bold; margin-bottom: 5px; } .message-text { font-size: 14px; } .message-time { font-size: 11px; opacity: 0.7; margin-top: 5px; } .typing-indicator { padding: 10px 20px; font-size: 13px; font-style: italic; color: #666; height: 30px; } .input-area { padding: 20px; border-top: 1px solid #e0e0e0; display: flex; gap: 10px; } #messageInput { flex: 1; padding: 12px; border: 1px solid #e0e0e0; border-radius: 20px; outline: none; font-size: 14px; } #sendButton { padding: 12px 30px; background: #0084ff; color: white; border: none; border-radius: 20px; cursor: pointer; font-weight: bold; } #sendButton:hover { background: #0073e6; } .system-message { text-align: center; color: #666; font-size: 13px; margin: 10px 0; font-style: italic; } </style> </head> <body> <div class="chat-container"> <div class="users-panel"> <h3>Online Users</h3> <div id="usersList"></div> </div> <div class="chat-panel"> <div class="chat-header"> <h2>Chat Room</h2> </div> <div id="messages" class="messages"></div> <div id="typingIndicator" class="typing-indicator"></div> <div class="input-area"> <input type="text" id="messageInput" placeholder="Type a message..."> <button id="sendButton">Send</button> </div> </div> </div> <script src="/socket.io/socket.io.js"></script> <script src="chat.js"></script> </body> </html>

Client-Side Chat Logic

Implement the JavaScript logic to handle chat functionality:

// chat.js - Client-Side Chat Logic let socket; let currentUser; let typingTimeout; let usersTyping = new Set(); // Get username from user const username = prompt('Enter your username:') || `User${Math.floor(Math.random() * 1000)}`; // Connect to server socket = io('http://localhost:3000', { auth: { username: username } }); // DOM elements const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); const usersList = document.getElementById('usersList'); const typingIndicator = document.getElementById('typingIndicator'); // Connection established socket.on('connected', (user) => { currentUser = user; console.log('Connected as:', user); }); // Display online users socket.on('onlineUsers', (users) => { usersList.innerHTML = users.map(user => `<div class="user-item">${user.username}</div>` ).join(''); }); // Load message history socket.on('messageHistory', (history) => { history.forEach(msg => displayMessage(msg)); scrollToBottom(); }); // New message received socket.on('newMessage', (message) => { displayMessage(message); scrollToBottom(); }); // User joined notification socket.on('userJoined', (user) => { displaySystemMessage(`${user.username} joined the chat`); // Add user to online list const userDiv = document.createElement('div'); userDiv.className = 'user-item'; userDiv.id = `user-${user.id}`; userDiv.textContent = user.username; usersList.appendChild(userDiv); }); // User left notification socket.on('userLeft', (user) => { displaySystemMessage(`${user.username} left the chat`); // Remove user from online list const userDiv = document.getElementById(`user-${user.id}`); if (userDiv) userDiv.remove(); // Remove from typing indicator if present usersTyping.delete(user.userId); updateTypingIndicator(); }); // Typing indicator socket.on('userTyping', (user) => { usersTyping.add(user.username); updateTypingIndicator(); }); socket.on('userStoppedTyping', (user) => { usersTyping.delete(user.username); updateTypingIndicator(); }); // Send message function sendMessage() { const message = messageInput.value.trim(); if (message) { socket.emit('sendMessage', message); messageInput.value = ''; socket.emit('stopTyping'); } } // Display message in chat function displayMessage(message) { const messageDiv = document.createElement('div'); messageDiv.className = message.userId === currentUser.id ? 'message own' : 'message'; const time = new Date(message.timestamp).toLocaleTimeString(); messageDiv.innerHTML = ` <div class="message-header">${message.username}</div> <div class="message-text">${escapeHtml(message.message)}</div> <div class="message-time">${time}</div> `; messagesDiv.appendChild(messageDiv); } // Display system message function displaySystemMessage(text) { const messageDiv = document.createElement('div'); messageDiv.className = 'system-message'; messageDiv.textContent = text; messagesDiv.appendChild(messageDiv); } // Update typing indicator function updateTypingIndicator() { if (usersTyping.size === 0) { typingIndicator.textContent = ''; } else if (usersTyping.size === 1) { typingIndicator.textContent = `${Array.from(usersTyping)[0]} is typing...`; } else { typingIndicator.textContent = `${usersTyping.size} people are typing...`; } } // Handle typing detection messageInput.addEventListener('input', () => { socket.emit('typing'); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { socket.emit('stopTyping'); }, 2000); }); // Send message on button click sendButton.addEventListener('click', sendMessage); // Send message on Enter key messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); // Utility functions function scrollToBottom() { messagesDiv.scrollTop = messagesDiv.scrollHeight; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
Tip: Always escape user-generated content before displaying it in HTML to prevent XSS (Cross-Site Scripting) attacks.

Adding Timestamps and Formatting

Enhance messages with better timestamp formatting:

// Utility functions for time formatting function formatTimestamp(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; // Less than 1 minute if (diff < 60000) { return 'Just now'; } // Less than 1 hour if (diff < 3600000) { const minutes = Math.floor(diff / 60000); return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; } // Today if (date.toDateString() === now.toDateString()) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } // Yesterday const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === yesterday.toDateString()) { return 'Yesterday ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } // Older return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }

Message Persistence with Database

In production, store messages in a database for persistence:

// Using MongoDB with Mongoose const mongoose = require('mongoose'); const messageSchema = new mongoose.Schema({ userId: String, username: String, message: String, timestamp: { type: Date, default: Date.now } }); const Message = mongoose.model('Message', messageSchema); // Connect to MongoDB mongoose.connect('mongodb://localhost/chat', { useNewUrlParser: true, useUnifiedTopology: true }); // Save message to database socket.on('sendMessage', async (message) => { const newMessage = new Message({ userId: socket.id, username: socket.username, message: message }); await newMessage.save(); io.emit('newMessage', { id: newMessage._id, userId: newMessage.userId, username: newMessage.username, message: newMessage.message, timestamp: newMessage.timestamp }); }); // Load message history from database socket.on('connection', async (socket) => { // Get last 100 messages const messages = await Message.find() .sort({ timestamp: -1 }) .limit(100) .exec(); socket.emit('messageHistory', messages.reverse()); });
Exercise: Enhance the chat application with these features:
  1. Add emoji support using an emoji picker library
  2. Implement message editing (double-click to edit your own messages)
  3. Add message deletion (right-click menu for your own messages)
  4. Display user avatars next to messages (use Gravatar or a placeholder service)
  5. Add "read receipts" showing when messages are seen by others
  6. Implement a "scroll to bottom" button that appears when scrolled up
  7. Add sound notifications for new messages
  8. Create a search functionality to find old messages

Test the application with multiple browser windows to simulate different users.