30 Days of JavaScript: WebSockets and Real-Time Communication, Day 26

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: WebSockets and Real-Time Communication, Day 26

Learn WebSockets for real-time communication. Discover how to set up a WebSocket server in Node.js and build an interactive, live chat app.

Load a chat app, type a message, and it shows up on someone else’s screen a fraction of a second later. No refresh, no “check for new messages” button. That instant back-and-forth is what WebSockets are for, and it’s Day 26 of our 30-day JavaScript journey. We’ll cover what WebSockets actually are, stand up a small server with Node.js, and wire it into a working chat page. By the end you’ll be able to add real-time features to your own projects, and you’ll know when they’re worth the trouble.

Table of Contents

Understanding WebSockets

What Are WebSockets?

A WebSocket is a persistent, two-way connection between a client and a server, running full-duplex over a single TCP connection. Regular HTTP is request and response: the client asks, the server answers, done. WebSockets keep one connection open so either side can send a message whenever it has something to say, in both directions at once.

How WebSockets Work

The connection starts life as an ordinary HTTP request. The client asks the server to upgrade, and if the server agrees, the same TCP connection switches over to the WebSocket protocol and stays open. From there it’s continuous data exchange in both directions, which is exactly what you want for live chat, stock tickers, and multiplayer games.

Example of WebSocket Handshake:

Bash
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

Once the connection is established, it carries both text and binary data, so it’s flexible enough for most real-time work.

Benefits of Using WebSockets
  • Real-Time Communication: Either side can push data the instant it has it, no polling loop required.
  • Lower Latency: The connection is already open, so you skip the cost of setting up a new request for every message.
  • Less Overhead Per Message: After the handshake, messages skip the full HTTP header on every exchange, so they’re cheaper than repeated requests.
  • Full-Duplex: Data flows both ways at the same time, instead of waiting for a response before the next exchange.

That said, WebSockets aren’t always the answer. If updates only flow one way, from server to client, Server-Sent Events (SSE) are simpler and reconnect on their own. If updates are rare, plain HTTP polling or long polling is easier to run and load-balance. Reach for WebSockets when you genuinely need fast, two-way traffic. Otherwise a lighter tool is less to maintain.

Common Use Cases for WebSockets
  • Chat Applications: Instant messaging lives or dies on low latency, and that’s WebSockets’ home turf.
  • Live Feeds: Financial tickers, sports scores, and news feeds push updates the moment they happen.
  • Online Gaming: Multiplayer games sync state between players in real time.
  • Collaborative Tools: Editors like Google Docs let several people work on the same document at once.

Setting Up a WebSocket Server with Node.js

Installing the Necessary Tools

You’ll need Node.js installed. For the server itself we’ll use ws, the go-to WebSocket library for Node.

Installation:

Bash
npm install ws

That pulls in the ws package, which handles creating and managing WebSocket connections for you.

Creating a Basic WebSocket Server

Start with the smallest thing that works. Create a file named server.js and drop in this code:

Example:

JS
/**
 * Basic WebSocket server in Node.js.
 * Listens for incoming connections, messages, and disconnections.
 */
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
  console.log('New client connected');
  ws.on('message', message => {
    console.log(`Received: ${message}`);
    ws.send(`You said: ${message}`);
  });
  ws.on('close', () => {
    console.log('Client disconnected');
  });
});
console.log('WebSocket server is running on ws://localhost:8080');

This server listens on port 8080. When a client connects, it logs the message it receives and echoes it straight back, then logs the disconnect when the client leaves. It’s an echo server, which is a nice way to prove the pipe works before you build anything real on top of it.

Running and Testing the WebSocket Server

Run it from your terminal:

Bash
node server.js

You can test it with a WebSocket client or straight from your browser’s developer console. The browser’s built-in WebSocket object gives you four handlers to work with: onopen, onmessage, onclose, and onerror. Here we use the first three:

Example:

JS
/**
 * Connects to a WebSocket server from the browser console.
 * Logs responses from the server.
 */
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
  console.log('Connected to server');
  ws.send('Hello Server!');
};
ws.onmessage = event => {
  console.log(`Message from server: ${event.data}`);
};
ws.onclose = () => {
  console.log('Disconnected from server');
};

This connects to the server, sends one message on open, and logs whatever comes back. Worth adding ws.onerror in real code so failed connections don’t fail silently.

Handling Multiple Clients

A single server can hold many connections at once. When one client sends a message, you can broadcast it to everyone else that’s connected.

Example:

JS
/**
 * WebSocket server with broadcasting functionality.
 * Broadcasts received messages to all connected clients except the sender.
 */
wss.on('connection', ws => {
  ws.on('message', message => {
    console.log(`Received: ${message}`);
    wss.clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
});

The readyState === WebSocket.OPEN check matters: it skips connections that are closing or already gone, so you don’t try to send into a dead socket. This is the whole idea behind a chat room, just with the sender left out of the broadcast.

Building a Real-Time Chat Application

Setting Up the Frontend

Now the front end. A plain HTML page is enough: an input, a send button, and a spot to show messages.

Example:

HTML
<!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>
</head>
<body>
  <h1>Chat Room</h1>
  <div id="chat"></div>
  <input type="text" id="message" placeholder="Type a message..." />
  <button onclick="sendMessage()">Send</button>
  <script>
    const ws = new WebSocket('ws://localhost:8080');
    ws.onmessage = event => {
      const chat = document.getElementById('chat');
      chat.innerHTML += `<p>${event.data}</p>`;
    };
    function sendMessage() {
      const input = document.getElementById('message');
      ws.send(input.value);
      input.value = '';
    }
  </script>
</body>
</html>

This connects to the server, sends whatever the user types, and appends incoming messages to the page. One honest caveat: dropping raw event.data into innerHTML is an XSS hole waiting to happen. It’s fine for a local demo, but before this goes anywhere real, escape the text or use textContent. More on that below.

Enhancing the Chat Application

To make it feel less like an echo box, you’ll want things like usernames, timestamps, and private messages. Here’s user identification, done by sending JSON instead of plain strings:

Example:

JS
/**
 * Enhanced WebSocket server with user identification.
 * Broadcasts messages with the sender's name.
 */
wss.on('connection', ws => {
  ws.on('message', message => {
    const parsedMessage = JSON.parse(message);
    const broadcastMessage = `${parsedMessage.name}: ${parsedMessage.text}`;
    wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(broadcastMessage);
      }
    });
  });
});

Each message now carries a name, so people can tell who said what. Wrap that JSON.parse in a try/catch in real code, because one malformed message shouldn’t take the whole server down. From here you can add message types to handle private messages, joins, and typing indicators.

Security Considerations

Real-time doesn’t get you out of the usual web security work. WebSockets are exposed to the same problems as the rest of your app, including cross-site scripting (XSS) and cross-site request forgery (CSRF). A few things that matter:

  • Validate and escape everything: Never trust incoming data. Validate it, and escape it before it hits the DOM. That’s the fix for the innerHTML issue above.
  • Use wss:// in production: The ws:// scheme is unencrypted, fine on localhost, not fine on the open internet. Use wss://, the TLS-encrypted version, for anything you ship.
  • Authenticate connections: WebSockets don’t send cookies the way you might expect, so check who’s connecting with a token or session, and verify the origin.
Scaling WebSocket Applications

Once you’re past one server, connections get tricky. Each WebSocket lives on the specific server it connected to, so a message on server A won’t reach a client on server B on its own. The usual fixes are load balancing, running more instances, and a message broker like Redis to pass messages between them.

Example:

Bash
# Using Redis Pub/Sub for scaling WebSocket applications
npm install redis

With Redis Pub/Sub in the middle, every server subscribes to the same channel, so a message published on one instance reaches clients connected to any of them. That’s what keeps a room consistent across a fleet of servers.

Deploying the Chat Application

Deploying means picking a host that actually supports long-lived WebSocket connections. Plenty do, including Heroku, AWS, and DigitalOcean. The rough shape of it:

  1. Pick a host that supports WebSockets: Something that runs Node and keeps connections open, like Heroku, AWS, or DigitalOcean.
  2. Get production-ready: Move config into environment variables, switch to wss://, and run under a process manager like PM2 so it restarts on crash.
  3. Ship it: Push your code up and start the app.

Conclusion

You’ve gone from what a WebSocket is to a running Node server and a chat page that updates live, plus the parts people usually skip: escaping user input, using wss://, authenticating connections, and scaling past one box with Redis. That’s the real difference between a demo and something you’d trust in production.

If you build on this, the next steps worth trying are private messaging, real authentication, and reconnect logic for when a connection drops. And remember the earlier point: WebSockets are the right tool for two-way, low-latency traffic. For one-way updates, SSE or polling is often less to run.

What's Next?

In Day 27, we’ll get into Server-Side Rendering with JavaScript: setting up SSR frameworks, managing server-rendered pages, and improving SEO for dynamic content.

Next: 30 Days of JavaScript: Server-Side Rendering with JavaScript, Day 27

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top