Node.js

Building a Simple Chat Server with Node.js and WebSocket

20250409 1425 Node.js Chat Server Simple Compose 01jrd8728tekps0yrwxe9a9t96 1024x683

Real-time communication is a core requirement for modern applications—from chat apps to live notifications to collaborative tools. With Node.js and WebSocket, you can build a lightweight chat server that delivers messages instantly without relying on third-party services.

In this guide, you’ll learn how to build a complete Node.js WebSocket chat server from scratch. We’ll cover the WebSocket protocol, implement a broadcast server, add user management, handle edge cases, and explore production considerations.

Why Use WebSocket for Real-Time Chat?

Traditional HTTP is request-response based. The client sends a request, the server responds, and the connection closes. For real-time features, this model falls short—you’d need constant polling, which wastes bandwidth and introduces latency.

WebSocket establishes a persistent, bidirectional connection. Once opened, both client and server can send messages at any time without the overhead of new HTTP requests.

HTTP vs WebSocket Comparison

Aspect HTTP Polling WebSocket
Connection New for each request Persistent
Latency Polling interval delay Near-instant
Bandwidth High (repeated headers) Low (small frames)
Server Push Not supported Native support
Complexity Simple to implement Slightly more setup

WebSocket is ideal for:

  • Chat applications
  • Live notifications
  • Real-time dashboards
  • Multiplayer games
  • Collaborative editing

Step 1: Project Setup

Create a new directory and initialize a Node.js project:

mkdir websocket-chat
cd websocket-chat
npm init -y

Install the required dependencies:

npm install ws uuid

The ws package is a simple, fast WebSocket implementation for Node.js. The uuid package helps generate unique identifiers for users.

Create the project structure:

websocket-chat/
├── server.js        # Main server file
├── client.html      # Test client
├── package.json
└── package-lock.json

Step 2: Create the Basic Chat Server

Start with a basic server that broadcasts messages to all connected clients:

// server.js
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');

const PORT = process.env.PORT || 8080;
const wss = new WebSocket.Server({ port: PORT });

// Store connected clients with metadata
const clients = new Map();

console.log(`WebSocket server started on ws://localhost:${PORT}`);

wss.on('connection', (ws, request) => {
  // Generate unique ID for this client
  const clientId = uuidv4();
  const clientIp = request.socket.remoteAddress;
  
  // Store client with metadata
  clients.set(ws, {
    id: clientId,
    ip: clientIp,
    username: `User-${clientId.slice(0, 4)}`,
    connectedAt: new Date()
  });
  
  console.log(`Client connected: ${clientId} from ${clientIp}`);
  console.log(`Total clients: ${clients.size}`);
  
  // Send welcome message to new client
  ws.send(JSON.stringify({
    type: 'system',
    message: `Welcome! You are connected as ${clients.get(ws).username}`,
    clientId: clientId,
    timestamp: new Date().toISOString()
  }));
  
  // Notify others about new user
  broadcast(ws, {
    type: 'system',
    message: `${clients.get(ws).username} joined the chat`,
    timestamp: new Date().toISOString()
  });
  
  // Handle incoming messages
  ws.on('message', (data) => {
    handleMessage(ws, data);
  });
  
  // Handle client disconnect
  ws.on('close', () => {
    const client = clients.get(ws);
    console.log(`Client disconnected: ${client.id}`);
    
    // Notify others about user leaving
    broadcast(ws, {
      type: 'system',
      message: `${client.username} left the chat`,
      timestamp: new Date().toISOString()
    });
    
    clients.delete(ws);
    console.log(`Total clients: ${clients.size}`);
  });
  
  // Handle errors
  ws.on('error', (error) => {
    console.error(`WebSocket error for ${clientId}:`, error.message);
  });
});

function handleMessage(senderWs, data) {
  const client = clients.get(senderWs);
  
  try {
    const message = JSON.parse(data.toString());
    
    switch (message.type) {
      case 'chat':
        // Broadcast chat message to all other clients
        broadcast(senderWs, {
          type: 'chat',
          username: client.username,
          message: message.content,
          timestamp: new Date().toISOString()
        });
        break;
        
      case 'setUsername':
        // Update username
        const oldUsername = client.username;
        client.username = message.username.slice(0, 20); // Limit length
        
        broadcast(null, {
          type: 'system',
          message: `${oldUsername} is now known as ${client.username}`,
          timestamp: new Date().toISOString()
        });
        break;
        
      default:
        console.log(`Unknown message type: ${message.type}`);
    }
  } catch (error) {
    console.error('Error parsing message:', error.message);
    // Handle plain text messages
    broadcast(senderWs, {
      type: 'chat',
      username: client.username,
      message: data.toString(),
      timestamp: new Date().toISOString()
    });
  }
}

function broadcast(excludeWs, message) {
  const messageStr = JSON.stringify(message);
  
  clients.forEach((clientData, clientWs) => {
    if (clientWs !== excludeWs && clientWs.readyState === WebSocket.OPEN) {
      clientWs.send(messageStr);
    }
  });
}

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('Shutting down server...');
  wss.clients.forEach((ws) => {
    ws.close(1001, 'Server shutting down');
  });
  wss.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

This server includes several production-ready features:

  • Unique client identification
  • Username management
  • Structured JSON messages
  • System notifications for join/leave events
  • Error handling
  • Graceful shutdown

Step 3: Create the Web Client

Build a complete HTML client with a better user interface:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket Chat</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { 
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #1a1a2e; 
      color: #eee;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    .header {
      background: #16213e;
      padding: 1rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .status { 
      padding: 0.5rem 1rem; 
      border-radius: 20px;
      font-size: 0.875rem;
    }
    .status.connected { background: #00b894; }
    .status.disconnected { background: #d63031; }
    .messages {
      flex: 1;
      overflow-y: auto;
      padding: 1rem;
    }
    .message {
      margin-bottom: 0.75rem;
      padding: 0.75rem;
      border-radius: 8px;
      max-width: 80%;
    }
    .message.chat { background: #16213e; }
    .message.system { 
      background: transparent;
      color: #888;
      font-style: italic;
      text-align: center;
      max-width: 100%;
    }
    .message .username {
      font-weight: bold;
      color: #74b9ff;
      margin-bottom: 0.25rem;
    }
    .message .time {
      font-size: 0.75rem;
      color: #666;
      margin-top: 0.25rem;
    }
    .input-area {
      background: #16213e;
      padding: 1rem;
      display: flex;
      gap: 0.5rem;
    }
    input {
      flex: 1;
      padding: 0.75rem;
      border: none;
      border-radius: 8px;
      background: #0f0f23;
      color: #eee;
      font-size: 1rem;
    }
    button {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 8px;
      background: #0984e3;
      color: white;
      cursor: pointer;
      font-size: 1rem;
    }
    button:hover { background: #74b9ff; }
    button:disabled { background: #555; cursor: not-allowed; }
  </style>
</head>
<body>
  <div class="header">
    <h1>WebSocket Chat</h1>
    <span id="status" class="status disconnected">Disconnected</span>
  </div>
  
  <div id="messages" class="messages"></div>
  
  <div class="input-area">
    <input id="messageInput" placeholder="Type a message..." autocomplete="off">
    <button id="sendBtn" onclick="sendMessage()">Send</button>
  </div>

  <script>
    let socket;
    let reconnectAttempts = 0;
    const maxReconnectAttempts = 5;
    
    function connect() {
      const wsUrl = 'ws://localhost:8080';
      socket = new WebSocket(wsUrl);
      
      socket.onopen = () => {
        console.log('Connected to server');
        reconnectAttempts = 0;
        updateStatus(true);
      };
      
      socket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        displayMessage(data);
      };
      
      socket.onclose = (event) => {
        console.log('Disconnected:', event.reason);
        updateStatus(false);
        attemptReconnect();
      };
      
      socket.onerror = (error) => {
        console.error('WebSocket error:', error);
      };
    }
    
    function attemptReconnect() {
      if (reconnectAttempts < maxReconnectAttempts) {
        reconnectAttempts++;
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
        console.log(`Reconnecting in ${delay/1000}s (attempt ${reconnectAttempts})`);
        setTimeout(connect, delay);
      }
    }
    
    function updateStatus(connected) {
      const status = document.getElementById('status');
      status.textContent = connected ? 'Connected' : 'Disconnected';
      status.className = 'status ' + (connected ? 'connected' : 'disconnected');
      document.getElementById('sendBtn').disabled = !connected;
    }
    
    function displayMessage(data) {
      const messages = document.getElementById('messages');
      const div = document.createElement('div');
      div.className = `message ${data.type}`;
      
      if (data.type === 'chat') {
        div.innerHTML = `
          <div class="username">${escapeHtml(data.username)}</div>
          <div>${escapeHtml(data.message)}</div>
          <div class="time">${formatTime(data.timestamp)}</div>
        `;
      } else {
        div.textContent = data.message;
      }
      
      messages.appendChild(div);
      messages.scrollTop = messages.scrollHeight;
    }
    
    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();
      
      if (message && socket.readyState === WebSocket.OPEN) {
        // Check for commands
        if (message.startsWith('/name ')) {
          socket.send(JSON.stringify({
            type: 'setUsername',
            username: message.slice(6)
          }));
        } else {
          socket.send(JSON.stringify({
            type: 'chat',
            content: message
          }));
          
          // Display own message locally
          displayMessage({
            type: 'chat',
            username: 'You',
            message: message,
            timestamp: new Date().toISOString()
          });
        }
        input.value = '';
      }
    }
    
    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }
    
    function formatTime(timestamp) {
      return new Date(timestamp).toLocaleTimeString();
    }
    
    // Handle Enter key
    document.getElementById('messageInput').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') sendMessage();
    });
    
    // Connect on page load
    connect();
  </script>
</body>
</html>

This client includes:

  • Automatic reconnection with exponential backoff
  • Connection status indicator
  • Username change via /name command
  • XSS protection through HTML escaping
  • Keyboard support (Enter to send)

Step 4: Running the Chat Server

Start the server:

node server.js

Open client.html in multiple browser tabs to simulate different users. Type messages and watch them appear in real-time across all connected clients.

Step 5: Adding Chat Rooms

For more complex applications, implement chat rooms:

// Room management
const rooms = new Map();

function joinRoom(ws, roomName) {
  const client = clients.get(ws);
  
  // Leave current room
  if (client.room) {
    leaveRoom(ws, client.room);
  }
  
  // Join new room
  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
  }
  rooms.get(roomName).add(ws);
  client.room = roomName;
  
  // Notify room
  broadcastToRoom(roomName, null, {
    type: 'system',
    message: `${client.username} joined ${roomName}`,
    timestamp: new Date().toISOString()
  });
}

function leaveRoom(ws, roomName) {
  const client = clients.get(ws);
  const room = rooms.get(roomName);
  
  if (room) {
    room.delete(ws);
    broadcastToRoom(roomName, ws, {
      type: 'system',
      message: `${client.username} left ${roomName}`,
      timestamp: new Date().toISOString()
    });
    
    // Clean up empty rooms
    if (room.size === 0) {
      rooms.delete(roomName);
    }
  }
}

function broadcastToRoom(roomName, excludeWs, message) {
  const room = rooms.get(roomName);
  if (!room) return;
  
  const messageStr = JSON.stringify(message);
  room.forEach((ws) => {
    if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
      ws.send(messageStr);
    }
  });
}

Production Considerations

Before deploying to production, consider these important factors:

Scaling with Multiple Servers

A single WebSocket server can’t scale horizontally without additional infrastructure. Use Redis Pub/Sub or a message broker to synchronize messages across multiple server instances.

Authentication

Add authentication during the WebSocket handshake:

wss.on('connection', (ws, request) => {
  // Verify token from query string or cookie
  const token = new URL(request.url, 'http://localhost').searchParams.get('token');
  
  if (!verifyToken(token)) {
    ws.close(4001, 'Unauthorized');
    return;
  }
  
  // Continue with connection setup...
});

Rate Limiting

Prevent abuse by limiting message frequency per client. Track message counts and temporarily block clients who exceed limits.

Message Persistence

For message history, store messages in a database (MongoDB, PostgreSQL) and load recent messages when clients connect.

Conclusion

You’ve built a complete Node.js WebSocket chat server with user management, structured messaging, and a functional web client. This foundation supports more advanced features like chat rooms, file sharing, message persistence, and authentication.

WebSocket’s persistent connection model makes it ideal for real-time applications. Combined with Node.js’s event-driven architecture, you can build scalable chat systems that handle thousands of concurrent connections.

For more backend development guides, see our posts on Building REST APIs with FastAPI and Mastering Async/Await in JavaScript. For WebSocket documentation, refer to the MDN WebSocket guide.