Blog Post

How to Build a Real-Time Chat App with Node.js and WebSockets

The Limits of Traditional HTTP

Standard web traffic uses the HTTP protocol, which operates on a strict "request-response" model. The client (your browser) asks for data, the server sends it, and the connection closes. If you are building a chat application, this is a terrible model. To see new messages, the browser would have to constantly ask the server, "Are there new messages yet?" every second. This is called "polling," and it wastes massive amounts of server resources.

Enter WebSockets

WebSockets solve this problem by establishing a persistent, bidirectional connection. Once a WebSocket tunnel is opened, the server can proactively push new messages down to the client the exact millisecond they arrive, without the client ever asking. This is how Discord, Slack, and WhatsApp work.

In the Node.js ecosystem, the gold standard for managing WebSockets is a library called Socket.IO.

Step 1: Setting Up the Node.js Server

First, we need to initialize a project and install Express and Socket.IO.

npm init -y
npm install express socket.io

Now, let's create our backend server. We need to attach the Socket.IO engine directly to our raw HTTP server so they share the same port.

// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

// Serve static HTML files from a 'public' folder
app.use(express.static('public'));

// Listen for incoming WebSocket connections
io.on('connection', (socket) => {
    console.log('A user connected with ID:', socket.id);

    // Listen for a custom event called 'chat message' from this specific user
    socket.on('chat message', (msg) => {
        // Broadcast the message to ALL connected users
        io.emit('chat message', msg);
    });

    socket.on('disconnect', () => {
        console.log('User disconnected');
    });
});

server.listen(3000, () => {
    console.log('Chat server running on http://localhost:3000');
});

Step 2: Building the Client Interface

Create a folder called public, and inside it, create an index.html. Socket.IO magically hosts its own client-side script, which we import directly in the HTML.

<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Real-Time Chat</title>
</head>
<body>
    <ul id="messages"></ul>
    <form id="form" action="">
        <input id="input" autocomplete="off" /><button>Send</button>
    </form>

    <!-- Import the Socket.IO client library -->
    <script src="/socket.io/socket.io.js"></script>
    <script>
        const socket = io(); // Connect to the server

        const form = document.getElementById('form');
        const input = document.getElementById('input');
        const messages = document.getElementById('messages');

        // When the user submits the form, emit the message to the server
        form.addEventListener('submit', (e) => {
            e.preventDefault();
            if (input.value) {
                socket.emit('chat message', input.value);
                input.value = '';
            }
        });

        // Listen for incoming messages broadcasted by the server
        socket.on('chat message', (msg) => {
            const item = document.createElement('li');
            item.textContent = msg;
            messages.appendChild(item);
            window.scrollTo(0, document.body.scrollHeight);
        });
    </script>
</body>
</html>

How the Magic Works

When you open this app in two different browser tabs, here is the exact flow of data:

  1. Tab 1 types "Hello!" and triggers socket.emit('chat message', 'Hello!').
  2. The Node.js server receives this via its socket.on listener.
  3. The server instantly triggers io.emit(), pushing the message down the WebSocket tunnel to every connected client.
  4. Tab 2's client-side listener instantly catches the event and renders the new HTML list item.

With just a few dozen lines of code, Socket.IO completely abstracts away the complexity of TCP connections, framing, and WebSocket upgrading, giving you a production-ready real-time communication pipeline.