Backend
WebSocket
Client & Server Implementation

Client & Server Implementation

Let's build a real-time chat application from the ground up using Node.js, the ws package, and a browser client. We will implement room management, structured JSON messaging protocols, authentication, and broadcasting.


1. Setting Up the Node.js Server

Install the standard ws library:

npm install ws

Basic Echo Server

import { WebSocketServer } from "ws";
 
const wss = new WebSocketServer({ port: 8080 });
 
console.log("⚡ WebSocket server listening on ws://localhost:8080");
 
wss.on("connection", (ws, req) => {
  const clientIp = req.socket.remoteAddress;
  console.log(`👤 New client connected from ${clientIp}`);
 
  // Send a welcome message upon connecting
  ws.send(JSON.stringify({
    type: "welcome",
    message: "Connected to real-time WebSocket server!"
  }));
 
  // Handle incoming data
  ws.on("message", (rawMessage) => {
    console.log("Received:", rawMessage.toString());
 
    // Echo back to the sender
    ws.send(JSON.stringify({
      type: "echo",
      message: rawMessage.toString()
    }));
  });
 
  // Handle disconnection
  ws.on("close", (code, reason) => {
    console.log(`❌ Client disconnected: Code ${code} - Reason: ${reason}`);
  });
 
  // Handle errors
  ws.on("error", (error) => {
    console.error("Socket error:", error);
  });
});

2. Browser Client Implementation

Here is an HTML5/JavaScript frontend client with auto-scroll and status indicators:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Real-Time WebSocket Chat</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
    #status { padding: 0.5rem; border-radius: 6px; font-weight: bold; margin-bottom: 1rem; }
    .connected { background: #d4edda; color: #155724; }
    .disconnected { background: #f8d7da; color: #721c24; }
    #chat-box { border: 1px solid #ccc; height: 350px; overflow-y: auto; padding: 1rem; border-radius: 6px; }
    .msg { margin: 0.4rem 0; padding: 0.5rem; border-radius: 4px; }
    .received { background: #e3f2fd; }
    .sent { background: #e8f5e9; text-align: right; }
    .system { background: #fff3cd; font-style: italic; }
    #input-group { display: flex; gap: 0.5rem; margin-top: 1rem; }
    #msg-input { flex: 1; padding: 0.6rem; border: 1px solid #ccc; border-radius: 4px; }
    button { padding: 0.6rem 1.2rem; cursor: pointer; border: none; background: #007bff; color: white; border-radius: 4px; }
  </style>
</head>
<body>
  <h2>Real-Time Chat</h2>
  <div id="status" class="disconnected">Connecting...</div>
  <div id="chat-box"></div>
  
  <div id="input-group">
    <input type="text" id="msg-input" placeholder="Type your message..." />
    <button id="send-btn">Send</button>
  </div>
 
  <script>
    const chatBox = document.getElementById("chat-box");
    const statusDiv = document.getElementById("status");
    const msgInput = document.getElementById("msg-input");
    const sendBtn = document.getElementById("send-btn");
 
    let socket;
 
    function connect() {
      socket = new WebSocket("ws://localhost:8080");
 
      socket.onopen = () => {
        statusDiv.textContent = "🟢 Connected";
        statusDiv.className = "connected";
        appendMessage("Connected to server", "system");
      };
 
      socket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        appendMessage(`${data.from || "Server"}: ${data.text || data.message}`, "received");
      };
 
      socket.onclose = () => {
        statusDiv.textContent = "đź”´ Disconnected. Retrying...";
        statusDiv.className = "disconnected";
        appendMessage("Disconnected from server", "system");
        setTimeout(connect, 3000); // Reconnect loop
      };
    }
 
    function appendMessage(text, className) {
      const el = document.createElement("div");
      el.className = `msg ${className}`;
      el.textContent = text;
      chatBox.appendChild(el);
      chatBox.scrollTop = chatBox.scrollHeight;
    }
 
    function sendMessage() {
      const text = msgInput.value.trim();
      if (!text || socket.readyState !== WebSocket.OPEN) return;
 
      socket.send(JSON.stringify({ type: "chat", text }));
      appendMessage(`Me: ${text}`, "sent");
      msgInput.value = "";
    }
 
    sendBtn.addEventListener("click", sendMessage);
    msgInput.addEventListener("keydown", (e) => { if (e.key === "Enter") sendMessage(); });
 
    connect();
  </script>
</body>
</html>

3. Implementing Chat Rooms & Broadcasting

To support rooms (e.g., #general, #engineering), use a Map of Sets to track room membership on the server:

Room Management Functions

import { WebSocketServer, WebSocket } from "ws";
 
const wss = new WebSocketServer({ port: 8080 });
const rooms = new Map(); // roomName -> Set<ws>
 
export function joinRoom(ws, roomName) {
  if (!rooms.has(roomName)) {
    rooms.set(roomName, new Set());
  }
  rooms.get(roomName).add(ws);
  ws.rooms = ws.rooms || new Set();
  ws.rooms.add(roomName);
 
  broadcastToRoom(roomName, {
    type: "notification",
    text: `User ${ws.userId} joined ${roomName}`
  }, ws); // Exclude the joiner
}
 
export function leaveRoom(ws, roomName) {
  const room = rooms.get(roomName);
  if (!room) return;
 
  room.delete(ws);
  ws.rooms?.delete(roomName);
 
  if (room.size === 0) {
    rooms.delete(roomName); // Clean up memory
  } else {
    broadcastToRoom(roomName, {
      type: "notification",
      text: `User ${ws.userId} left ${roomName}`
    });
  }
}
 
export function broadcastToRoom(roomName, payload, excludeSocket = null) {
  const room = rooms.get(roomName);
  if (!room) return;
 
  const data = JSON.stringify(payload);
  room.forEach((client) => {
    if (client !== excludeSocket && client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

4. Structured Message Protocol Design

Instead of sending raw unstructured strings, always use a typed message envelope:

{
  "type": "chat_message",
  "payload": {
    "room": "general",
    "text": "Hey everyone!",
    "timestamp": 1718000000
  },
  "meta": {
    "userId": "usr_9981",
    "clientMsgId": "msg_abc123"
  }
}

Router Pattern on Server

const messageHandlers = {
  join_room: (ws, payload) => joinRoom(ws, payload.room),
  leave_room: (ws, payload) => leaveRoom(ws, payload.room),
  chat_message: (ws, payload) => {
    broadcastToRoom(payload.room, {
      type: "chat_message",
      from: ws.username,
      room: payload.room,
      text: payload.text,
      timestamp: Date.now()
    });
  }
};
 
ws.on("message", (raw) => {
  try {
    const { type, payload } = JSON.parse(raw);
    const handler = messageHandlers[type];
    if (handler) {
      handler(ws, payload);
    } else {
      console.warn("Unknown message type:", type);
    }
  } catch (err) {
    ws.send(JSON.stringify({ type: "error", message: "Malformed JSON message" }));
  }
});

5. WebSocket Authentication Strategies

Strategy A: Token in URL Query Parameter (Handshake Auth)

The client passes a JWT inside the connection URL during the initial HTTP handshake:

// Client
const ws = new WebSocket("wss://example.com/ws?token=eyJhbGciOi...");
 
// Server
import url from "url";
import jwt from "jsonwebtoken";
 
wss.on("connection", (ws, req) => {
  const queryParams = new URLSearchParams(url.parse(req.url).query);
  const token = queryParams.get("token");
 
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    ws.user = user;
    ws.userId = user.id;
  } catch (err) {
    // Terminate unauthorized connection immediately
    ws.close(1008, "Unauthorized: Invalid or expired token");
  }
});

Strategy B: First-Message Authentication (Post-Connection)

For scenarios where query parameters might be logged in web server access logs, require the client to send an authentication payload within 5 seconds of connecting:

wss.on("connection", (ws) => {
  ws.isAuthenticated = false;
 
  // Set 5-second auth timeout
  const authTimeout = setTimeout(() => {
    if (!ws.isAuthenticated) {
      ws.close(1008, "Auth Timeout: Token not provided within 5s");
    }
  }, 5000);
 
  ws.on("message", (raw) => {
    const data = JSON.parse(raw);
 
    if (!ws.isAuthenticated) {
      if (data.type === "auth" && verifyToken(data.token)) {
        ws.isAuthenticated = true;
        clearTimeout(authTimeout);
        ws.send(JSON.stringify({ type: "auth_success" }));
      } else {
        ws.close(1008, "Invalid credentials");
      }
      return;
    }
 
    // Process regular messages only after authentication succeeds
    handleMessage(ws, data);
  });
});

6. Complete Production Chat Server (server.js)

import http from "http";
import { WebSocketServer, WebSocket } from "ws";
 
const server = http.createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    return res.end(JSON.stringify({ status: "healthy", connections: wss.clients.size }));
  }
  res.writeHead(404);
  res.end();
});
 
const wss = new WebSocketServer({ server });
const rooms = new Map();
 
wss.on("connection", (ws) => {
  ws.isAlive = true;
  ws.userId = `usr_${Math.random().toString(36).substring(7)}`;
  ws.rooms = new Set();
 
  ws.on("pong", () => { ws.isAlive = true; });
 
  ws.on("message", (raw) => {
    try {
      const { action, room, text } = JSON.parse(raw);
 
      if (action === "join") {
        if (!rooms.has(room)) rooms.set(room, new Set());
        rooms.get(room).add(ws);
        ws.rooms.add(room);
        broadcast(room, { type: "system", text: `${ws.userId} joined ${room}` });
      }
 
      if (action === "message" && ws.rooms.has(room)) {
        broadcast(room, { type: "chat", from: ws.userId, room, text, timestamp: Date.now() });
      }
    } catch (err) {
      ws.send(JSON.stringify({ error: "Invalid JSON format" }));
    }
  });
 
  ws.on("close", () => {
    ws.rooms.forEach((r) => {
      const roomSet = rooms.get(r);
      roomSet?.delete(ws);
      if (roomSet?.size === 0) rooms.delete(r);
      else broadcast(r, { type: "system", text: `${ws.userId} left` });
    });
  });
});
 
function broadcast(room, payload) {
  const roomSet = rooms.get(room);
  if (!roomSet) return;
  const msg = JSON.stringify(payload);
  roomSet.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) client.send(msg);
  });
}
 
// 30-second ping interval
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);
 
wss.on("close", () => clearInterval(interval));
 
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => console.log(`🚀 Chat server active on port ${PORT}`));

👉 Next Step: Learn how to scale across multiple server instances using Redis Pub/Sub in Scaling WebSockets with Redis Pub/Sub.