WebSockets with Node JS - Create a chat application

WebSockets with Node JS - Create a chat application

Introduction

WebSockets allow real-time functionality in apps by allowing bidirectional, small data transfers over a single persistent connection. They are easy to use in the front end as they have built-in APIs in modern browsers, and some libraries make them even more convenient to use.

In this guide, I'll show you how to get started with WebSockets by creating a simple chat application using NodeJS and documenting each step. I've written and tested the code using the latest LTS version of NodeJS, which at the moment of writing this article, is v18.13.0. Make sure it's already installed on your computer, otherwise, go to the NodeJS download page and get it up and running.

If you want to know more about WebSockets, I invite you to read "WebSockets vs HTTP - What WebSockets are and how they differ from HTTP connections".

Create a basic index.html page for the client side

First, we need to create a new folder to store our chat application files and inside, create and open with your editor of choice, the index.html.

For now, we start by adding just a basic template structure inside but no JavaScript.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <title>Simple Chat application</title>
</head>

<body class="d-flex flex-column vh-100">
    <div class="border-bottom mb-auto">
        <div class="container">
            <div class="row my-2">
                <div class="col-12">
                    <h1>Chat</h1>
                </div>
            </div>
        </div>
    </div>
    <div class="container h-100 overflow-y-scroll" id="messages-container">
        <div class="row">
            <div class="col-12" id="messages">
                <!-- Messages will go here -->
            </div>
        </div>        
    </div>
    <div class="border-top mt-auto">
        <div class="container">
            <div class="row my-2">
                <div class="col-12">
                    <form>
                        <div class="input-group">
                            <input 
                                type="text" 
                                class="form-control" 
                                placeholder="Message"
                                id="message">
                            <button 
                                class="btn btn-primary"
                                type="submit"
                                id="send">
                                Send
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

The code includes the addition of Bootstrap within the "head" tag to utilise some CSS utility classes to improve the interface and enhance ease of use.

Within the "body" tag, we included a top section for the chat header, a middle section for displaying messages, and a bottom section consisting of a simple form for sending messages.

If you're familiar with Bootstrap, you'll notice the use of flexboxes to properly align and display the three sections that form the UI,  and the use of the class "overflow-y-scroll" applied to the "messages-container" section to ensure easy scrolling of chat messages.

Create a Web Server in NodeJS to serve our HTML file

The last file we'll add to our project is the server-side javascript file "server.js" which will contain both the code for the HTTP server and the WebSocket server.

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
    fs.readFile('index.html', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end(`Error getting the file: ${err}.`);
        } else {
            res.statusCode = 200;
            res.setHeader('Content-type', 'text/html');
            res.end(data);
        }
    });
});

server.listen(8000);
console.log('Chat app address: http://localhost:8000');

With the above code, we're creating a simple HTTP server that listens on port 8000 of our computer, and whenever it receives a request, it retrieves the index.html file we created earlier. If the server doesn't encounter any errors, it will send the file content back to the browser.

After saving the file, we need to open the terminal, open the project directory and run "node server.js". Then, by opening http://localhost:8000 on the browser, we should see the UI created in the previous steps.

Even if you try to interact with it, you'll notice that it doesn't do anything for now but it will in a moment once we add the code to handle WebSocket connections on both the server and the client.

WebSocket server implementation

Make sure the server is not running and then in the terminal run: "npm install ws". Once the process has been completed, update the "server.js" file with the following code:

const http = require('http');
const fs = require('fs');
const WebSocket = require('ws');

const server = http.createServer((req, res) => {
    fs.readFile('index.html', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end(`Error getting the file: ${err}.`);
        } else {
            res.statusCode = 200;
            res.setHeader('Content-type', 'text/html');
            res.end(data);
        }
    });
});

server.listen(8000);
console.log('Chat app address: http://localhost:8000');


const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
    ws.on('message', (message) => {
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                const messageWithTimestamp = {
                    message: message.toString(),
                    time: new Date().toISOString(),
                };
                client.send(JSON.stringify(messageWithTimestamp));
            }
        });
    });
});

In the above code, we imported the "ws library" that we downloaded using npm install and then we instantiated a new WebSocket server running on port 8080.

At every WebSocket connection opened, it listens for the message event, then it iterates through all the connected clients to broadcast the message.

The message broadcasted is a JSON string from the object containing both the actual message and the time when the server received it.

The server-side code is now completed, we can now start the server by running "node server.js" on the terminal.

WebSocket client implementation

To send messages in our chat application, we need to update our index.html with the following code:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <title>Simple Chat application</title>
</head>

<body class="d-flex flex-column vh-100">
    <div class="border-bottom mb-auto">
        <div class="container">
            <div class="row my-2">
                <div class="col-12">
                    <h1>Chat</h1>
                </div>
            </div>
        </div>
    </div>
    <div class="container h-100 overflow-y-scroll" id="messages-container">
        <div class="row">
            <div class="col-12" id="messages">
                <!-- Messages will go here -->
            </div>
        </div>        
    </div>
    <div class="border-top mt-auto">
        <div class="container">
            <div class="row my-2">
                <div class="col-12">
                    <form onsubmit="sendMessage();return false">
                        <div class="input-group">
                            <input 
                                type="text" 
                                class="form-control" 
                                placeholder="Message"
                                id="message">
                            <button 
                                class="btn btn-primary"
                                type="submit"
                                id="send">
                                Send
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <script>
        const socket = new WebSocket('ws://localhost:8080');

        socket.onopen = () => {
            console.log('Connected to the chat server');
        };

        socket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            console.log(`Received message: `, data);
            const messageTemplate = document.createElement('template');
            messageTemplate.innerHTML = `
                <div class="card w-100 my-2">
                    <div class="card-body">
                        <h6 class="card-subtitle mb-2 text-muted">${data.time}</h6>
                        <p class="card-text">${data.message}</p>
                    </div>
                </div>
            `;
            const messagesEl = document.getElementById('messages');
            messagesEl.appendChild(messageTemplate.content);
            const messagesContainerEl = document.getElementById('messages-container');
            messagesContainerEl.scrollTop = messagesContainerEl.scrollHeight;
        };

        socket.onclose = () => {
            console.log('Disconnected from the chat server');
        };

        // Sending message to the server
        function sendMessage() {
            const messageEl = document.getElementById('message');
            socket.send(messageEl.value);
            messageEl.value = '';
        }
    </script>
</body>
</html>

As you might have noticed, we added the attribute "onsubmit" to the form that will call the function "sendMessage()". Also, the addition of "return false;" will prevent the reload of the entire page when the form is submitted.

Before closing the "body" tag, we added our script to handle the interactions on the page and the connection to the WebSocket server.

In our JavaScript code, we first established a new WebSocket client connection and then we set up listeners for the events "onopen", "onmessage", and "onclose".

The onopen event is triggered when the client connects to the server. The onmessage event is triggered when a message is received from the server. The onclose event is triggered when the connection is closed. These events allow the client to handle different states and messages of the WebSocket connection.

When the onmessage event is triggered, the following happens:

  • We parse the event data into a JavaScript object.
  • We create the HTML element that contains the date and message.
  • We add the previously created element to our page inside the element with id: "messages".
  • We select the scrollable container of the messages with the id: "messages-container", telling it to scroll to the bottom.

When "sendMessage()" is called, we send the message to the server by calling "socket.send()" and passing the value of the input field as an argument. Finally, we reset the value of the input form.

Conclusion

In this tutorial, we used the HTTP protocol to serve the index.html file containing the client code and the WebSocket protocol to send and receive messages in real-time.

The above code is far from perfect, but I hope it can be a good starting point for you to learn more about WebSockets. Don't forget to check the ws API docs.