tutorial // Jun 01, 2021

How to Set Up a Websocket Server with Node.js and Express

How to attach a websocket server to an existing Express server to add real-time data to your app.

How to Set Up a Websocket Server with Node.js and Express

Getting started

For this tutorial, we're going to be using the CheatCode Node.js Boilerplate. This will give us access to an existing Express server that we can attach our websocket server to:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

After you've cloned the project, cd into it and install its dependencies:

Terminal

cd nodejs-server-boilerplate && npm install

Finally, for this tutorial, we need to install two additional dependencies: ws for creating our websocket server and query-string for parsing query params from our websocket connections:

Terminal

npm i ws query-string

After this, start up the development server:

Terminal

npm run dev

Creating a websocket server

To begin, we need to set up a new websocket server that can handle inbound websocket requests from clients. First, in the /index.js file of the project we just cloned, let's add a call to the function that will setup our websocket server:

/index.js

import express from "express";
import startup from "./lib/startup";
import api from "./api/index";
import middleware from "./middleware/index";
import logger from "./lib/logger";
import websockets from './websockets';

startup()
  .then(() => {
    const app = express();
    const port = process.env.PORT || 5001;

    middleware(app);
    api(app);

    const server = app.listen(port, () => {
      if (process.send) {
        process.send(`Server running at http://localhost:${port}\n\n`);
      }
    });

    websockets(server);

    process.on("message", (message) => {
      console.log(message);
    });
  })
  .catch((error) => {
    logger.error(error);
  });

Here, we've imported a hypothetical websockets function from ./websockets which is anticipating an index.js file at that path (Node.js interprets this as ./websockets/index.js). Inside of the .then() callback for our server startup() function, we've added a call to this function just beneath our call to app.listen(). To it, we pass server which is the HTTP server returned by Express when the HTTP server is opened on the passed port (in this case 5001).

Once server is available, we call to our websockets() function, passing in the HTTP server (this is what we'll attach the websocket server to that we'll create in the next section).

Attaching a websocket server to an express server

Next, we need to create the /websockets/index.js file that we assumed will exist above. To keep our code clean, we're going to create a separate websockets directory at the root of the project we cloned and create an index.js file inside of that:

/websockets/index.js

import WebSocket from "ws";

export default (expressServer) => {
  const websocketServer = new WebSocket.Server({
    noServer: true,
    path: "/websockets",
  });

  return websocketServer;
};

Here, we export a function that takes in a single argument of expressServer which contains the Express app instance that we intend to pass in when we call the function from /index.js at the root of the project.

Just inside that function, we create our websocket server using the Websocket.Server constructor from the ws package that we installed above. To that constructor, we pass the noServer option as true to say "do not set up an HTTP server alongside this websocket server." The advantage to doing this is that we can share a single HTTP server (i.e., our Express server) across multiple websocket connections. We also pass a path option to specify the path on our HTTP server where our websocket server will be accessible (ultimately, localhost:5001/websockets).

/websockets/index.js

import WebSocket from "ws";

export default async (expressServer) => {
  const websocketServer = new WebSocket.Server({
    noServer: true,
    path: "/websockets",
  });

  expressServer.on("upgrade", (request, socket, head) => {
    websocketServer.handleUpgrade(request, socket, head, (websocket) => {
      websocketServer.emit("connection", websocket, request);
    });
  });

  return websocketServer;
};

Extending our code, next, we need to handle the attachment of the websocket server to the existing expressServer. To do it, on the expressServer we listen for an upgrade event. This event is fired whenever our Express server—a plain HTTP server—receives a request for an endpoint using the websockets protocol. "Upgrade" here is saying, "we need to upgrade this request to handle websockets."

Passed to the callback for the event handler—the .on('upgrade') part—we have three arguments request, socket, and head. request represents the inbound HTTP request that was made from a websocket client, socket represents the network connection between the browser (client) and the server, and head represents the first packet/chunk of data for the inbound request.

Next, inside the callback for the event handler, we make a call to websocketServer.handleUpgrade(), passing along with the request, socket, and head. What we're saying with this is "we're being asked to upgrade this HTTP request to a websocket request, so perform the upgrade and then return the upgraded connection to us."

That upgraded connection, then, is passed to the callback we've added as the fourth argument to websocketServer.handleUpgrade(). With that upgraded connection, we need to handle the connection—to be clear, this is the now-connected websocket client connection. To do it, we "hand off" the upgraded connection websocket and the original request by emitting an event on the websocketServer with the name connection.

Handling inbound websocket connections

At this point, we've upgraded our existing Express HTTP server, however, we haven't completely handled the inbound request. In the last section, we got up to the point where we're able to upgrade the inbound HTTP request from a websocket client into a true websocket connection, however, we haven't handled that connection.

/websockets/index.js

import WebSocket from "ws";
import queryString from "query-string";

export default async (expressServer) => {
  const websocketServer = new WebSocket.Server({[...]});

  expressServer.on("upgrade", (request, socket, head) => {[...]});

  websocketServer.on(
    "connection",
    function connection(websocketConnection, connectionRequest) {
      const [_path, params] = connectionRequest?.url?.split("?");
      const connectionParams = queryString.parse(params);

      // NOTE: connectParams are not used here but good to understand how to get
      // to them if you need to pass data with the connection to identify it (e.g., a userId).
      console.log(connectionParams);

      websocketConnection.on("message", (message) => {
        const parsedMessage = JSON.parse(message);
        console.log(parsedMessage);
      });
    }
  );

  return websocketServer;
};

To handle that connection, we need to listen for the connection event that we emitted in the last section. To do it, we make a call to websocketServer.on('connection') passing it a callback function that will handle the inbound websocket connection and the accompanying request.

To clarify, the difference between the websocketConnection and the connectionRequest is that the former represents the open, long-running network connection between the browser and the server, while the connectionRequest represents the original request to open that connection.

Focusing on the callback we've passed to our .on('connection') handler, we do something special. Per the implementation for websockets, there is no way to pass data (e.g., a user's ID or some other identifying information) in the body of a websocket request (similar to how you can pass a body with an HTTP POST request).

Instead, we need to include any identifying information in the query params of the URL of our websocket server when connecting to the server via a websocket client (more on this in the next section). Unfortunately, these query params are not parsed by our websocket server and so we need to do this manually.

To extract the query params into a JavaScript object, from the connectionRequest, we grab the URL the request was made for (this is the URL the websocket client makes the connection request to) and split it at the ?. We do this because we don't care about any part of the URL before and up to the ?, or, our query params in URL form.

Using JavaScript array destructuring, we take the result of our .split('?') and assume that it returns an array with two values: the path portion of the URL and the query params in URL form. Here, we label the path as _path to suggest that we're not using that value (prefixing an _ underscore to a variable name is a common way to denote this across programming languages). Then, we "pluck off" the params value that was split off from the URL. To be clear, assuming the URL in the request looks like ws://localhost:5001/websockets?test=123&test2=456 we expect something like this to be in the array:

['ws://localhost:5001/websockets', 'test=123&test2=456']

As they exist, the params (in the example above test=123&test2=456) are unusable in our code. To make them usable, we pull in the queryString.parse() method from the query-string package that we installed earlier. This method takes a URL-formatted query string and converts it into a JavaScript object. The end result considering the example URL above would be:

{ test: '123', test2: '456' }

With this, now we can reference our query params in our code via the connectionParams variable. We don't do anything with those here, but this information is included because frankly, it's frustrating to figure that part out.

/websockets/index.js

import WebSocket from "ws";
import queryString from "query-string";

export default async (expressServer) => {
  const websocketServer = new WebSocket.Server({
    noServer: true,
    path: "/websockets",
  });

  expressServer.on("upgrade", (request, socket, head) => {
    websocketServer.handleUpgrade(request, socket, head, (websocket) => {
      websocketServer.emit("connection", websocket, request);
    });
  });

  websocketServer.on(
    "connection",
    function connection(websocketConnection, connectionRequest) {
      const [_path, params] = connectionRequest?.url?.split("?");
      const connectionParams = queryString.parse(params);

      // NOTE: connectParams are not used here but good to understand how to get
      // to them if you need to pass data with the connection to identify it (e.g., a userId).
      console.log(connectionParams);

      websocketConnection.on("message", (message) => {
        const parsedMessage = JSON.parse(message);
        console.log(parsedMessage);
        websocketConnection.send(JSON.stringify({ message: 'There be gold in them thar hills.' }));
      });
    }
  );

  return websocketServer;
};

Above, we have our completed websocket server implementation. What we've added is an event handler for when our websocketConnection receives an inbound message (the idea of websockets is to keep a long-running connection open between the browser and the server across which messages can be sent back and forth).

Here, when a message event comes in, in the callback passed to the event handler, we take in a single message property as a string. Here, we're assuming that our message is a stringified JavaScript object, so we use JSON.parse() to convert that string into a JavaScript object that we can interact with in our code.

Finally, to showcase responding to a message from the server, we call to websocketConnection.send(), passing a stringified object back (we'll assume the client is also anticipating a stringified JavaScript object being passed in its inbound messages).

Testing out the websocket server

Because we're not showcasing how to set up a websocket client in a front-end in this tutorial, we're going to use a Chrome/Brave browser extension called Smart Websocket Client that gives us a pseudo front-end that we can use to test things out.

HZhEqF3ZCMTYsMtH/OG5Iw6vTqKGsfWxy.0
Testing out our websocket server in the Smart Websocket Client browser extension.

On top, we have our running HTTP/websocket server running in a terminal (this is the development server of the projet we cloned at the beginning of this project) and on the bottom, we have the Smart Websocket Client extension opened up in the browser (Brave).

First, we enter the URL where we expect our websocket server to exist. Notice that instead of the usual http:// that we prefix to a URL when connecting to a server, because we want to open a websocket connection, we prefix our URL with ws:// (similarly, in production, if we have SSL enabled we'd want to use wss:// for "websockets secure").

Because we expect our server to be running on port 5001 (the default port for the project we're building this on top of and where our HTTP server is accepting requests), we use localhost:5001, followed by /websockets?userId=123 to say "on this server, navigate to the /websockets path where our websocket server is attached and include the query param userId set to the value 123."

When we click the "Connect" button in the extension, we get an open connection to our websocket server. Next, to test it out, in the text area beneath the "Send" button, we enter a pre-written stringified object (created by running JSON.stringify({ howdy: "tester" }) in the browser console) and then click the "Send" button to send that stringified object up to the server.

If we watch the server terminal at the top, we can see the userId query param being parsed from the URL when we connect and when we send a message, we see that message logged out on the server and get the expected { message: "There be gold in them thar hills." } message in return on the client.

Wrapping up

In this tutorial, we learned how to set up a websocket server and attach it to an existing Express HTTP server. We learned how to initialize the websocket server and then use the upgrade event on inbound connection requests to support the websockets protocol.

Finally, we looked at how to send and receive messages to our connected clients and how to use JSON.stringify() and JSON.parse() to send objects via websockets.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode