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.
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.
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.