Tutorial · JavaScript

Build a Real-Time Chat App with WebSockets

Joy Dey July 1, 2026 9 min read JavaScript WebSockets Node.js Tutorial

HTTP is a request-response protocol — the client asks, the server responds, and the connection closes. This works perfectly for fetching data, but it falls apart when you need bidirectional, low-latency communication: think chat apps, live dashboards, multiplayer games, or collaborative editors.

That's exactly where WebSockets shine. A WebSocket connection stays open, allowing both the server and the client to push data at any time without the overhead of repeatedly establishing new connections. In this tutorial, we'll build a real-time multi-user chat room from scratch — no Socket.io, no heavy libraries — just the native WebSocket API and a simple Node.js server.

Step 1: Setting Up the Node.js WebSocket Server

We'll use the ws package, which is a fast, minimal WebSocket library for Node.js. Create a project folder and initialise it:

mkdir ws-chat && cd ws-chat
npm init -y
npm install ws

Create a server.js file. The server's job is to accept incoming connections and broadcast every message it receives to all connected clients:

// server.js
const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Client connected. Total:', wss.clients.size);

  // When a message arrives, broadcast it to every connected client
  ws.on('message', (data) => {
    const message = data.toString();

    wss.clients.forEach((client) => {
      // ReadyState 1 = OPEN
      if (client.readyState === 1) {
        client.send(message);
      }
    });
  });

  ws.on('close', () => {
    console.log('Client disconnected. Total:', wss.clients.size);
  });

  // Send a welcome message to the new client only
  ws.send(JSON.stringify({ type: 'system', text: 'Welcome to the chat!' }));
});

console.log('WebSocket server running on ws://localhost:8080');

Start the server with:

node server.js

Step 2: The HTML Structure

Create an index.html file that contains the chat UI — a message list, a username input, a message input, and a send button.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>WebSocket Chat</title>
  <style>
    body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
    #messages { border: 1px solid #ddd; border-radius: 8px; height: 350px;
                overflow-y: auto; padding: 1rem; margin-bottom: 1rem; background: #f9f9f9; }
    .msg { margin-bottom: 0.5rem; }
    .msg strong { color: #6c63ff; }
    .msg.system { color: #999; font-style: italic; }
    .controls { display: flex; gap: 0.5rem; }
    input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #ccc; border-radius: 6px; }
    button { padding: 0.5rem 1rem; background: #6c63ff; color: #fff;
             border: none; border-radius: 6px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>💬 Live Chat</h1>
  <div id="messages"></div>
  <div class="controls">
    <input type="text" id="username" placeholder="Your name" style="max-width:130px">
    <input type="text" id="msg-input" placeholder="Type a message..." />
    <button id="send-btn">Send</button>
  </div>
  <script src="app.js"></script>
</body>
</html>

Step 3: The Client-Side JavaScript

Create app.js. The browser's native WebSocket constructor takes a URL and gives you four event handlers to work with: onopen, onmessage, onerror, and onclose.

// app.js
const ws = new WebSocket('ws://localhost:8080');

const messagesEl = document.getElementById('messages');
const msgInput   = document.getElementById('msg-input');
const sendBtn    = document.getElementById('send-btn');
const usernameEl = document.getElementById('username');

// Helper to append a message to the list
function appendMessage({ type, sender, text }) {
  const div = document.createElement('div');
  div.className = 'msg' + (type === 'system' ? ' system' : '');

  if (type === 'system') {
    div.textContent = text;
  } else {
    div.innerHTML = `<strong>${escapeHtml(sender)}</strong>: ${escapeHtml(text)}`;
  }

  messagesEl.appendChild(div);
  messagesEl.scrollTop = messagesEl.scrollHeight;
}

// Basic XSS guard
function escapeHtml(str) {
  return str.replace(/[<>&"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]));
}

// Receive messages from the server
ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  appendMessage(data);
};

ws.onopen  = () => console.log('Connected to server');
ws.onerror = (err) => console.error('WebSocket error:', err);
ws.onclose = () => appendMessage({ type: 'system', text: 'Disconnected from server.' });

// Send a message
function sendMessage() {
  const text   = msgInput.value.trim();
  const sender = usernameEl.value.trim() || 'Anonymous';
  if (!text || ws.readyState !== WebSocket.OPEN) return;

  ws.send(JSON.stringify({ type: 'chat', sender, text }));
  msgInput.value = '';
}

sendBtn.addEventListener('click', sendMessage);
msgInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendMessage(); });

Notice that we're sending structured JSON objects instead of plain strings. This gives us a type field we can use to style system messages differently from user chat messages.

Step 4: Broadcasting Usernames on the Server

Our server currently just forwards whatever string it receives. Since we're now sending JSON with a sender field, the server should forward the entire parsed object. Update server.js to handle this gracefully:

ws.on('message', (data) => {
  let parsed;
  try {
    parsed = JSON.parse(data.toString());
  } catch {
    return; // ignore malformed messages
  }

  const payload = JSON.stringify(parsed);

  wss.clients.forEach((client) => {
    if (client.readyState === 1) {
      client.send(payload);
    }
  });
});

Step 5: Running & Testing the App

Open two steps in your terminal:

  1. Start the server: node server.js
  2. Open index.html in two separate browser tabs (you can use Live Server in VS Code, or just open the file directly).

Type a message in one tab and click Send — it should appear instantly in both tabs. You now have a working real-time chat application with zero framework overhead.

Conclusion

WebSockets are one of the most powerful primitives available in modern browsers. By keeping a persistent connection open, you eliminate the polling overhead that plagued older real-time approaches and enable truly instant, bidirectional communication. From here you could extend this app by adding rooms, user presence indicators, message history stored in a database, or TLS encryption for production deployment.