Easily integrate WebSockets into your ExpressJS application.
yarn add wsexpress
npm install wsexpress
import {Request, Response} from "express";
import {wsExpress, listen} from "wsexpress";
const express = require("express");
// Create express application
const app = express();
// Use the wsExpress middleware in your Express application
// Pass the `app` to the wsExpress middleware
app.use(wsExpress({ app }));
// Define your routes
app.get('/', (request: Request, response: Response) => {
response.status(200).send();
});
// Use the wsExpress middleware's `listen` function to start the HTTP server
// Pass the `app` to the `listen` function
listen({ app }, 8080, () => {
console.log('listening');
});
The middleware returns an "intermediate" HTTP request handler - this is simply
to satisfy the Express app's use
method. This request handler simply forwards
the incoming HTTP request to the next route in the stack using next()
. You
may use your own request handler if you'd like, supply it in the configuration
object passed to wsExpress()
:
app.use(wsExpress({
app,
requestHander: (request: Request, response: Response, next: NextFunction) => {
// Your handler
},
}))
The middleware handles two major components of the request lifecycle:
- Handling WS upgrade requests
- Routing WS messages
The middleware will first process an upgrade request to create a websocket client connection. Once the client sends a message, the middleware will route that message to the appropriate Express route.
At this point Express takes over briefly, running the logic within the route
then calling methods on the response
object to ultimately generate a response
to the client.
The middleware overrides the response
object's send
method - "hijacking" it
to send the response's body to the websocket client.
This package includes a listen
function intended to replace the app.listen
call that is normally used with Express applications. This is because the WebSocket
server needs the http.Server
instance that is returned only when app.listen
is called.
The listen
function is nearly identical to the app.listen
method, except it
requires a configuration object to be passed in, including the app
:
listen(
{ app }, // The Express application
8080, // Port to listen on
() => {
// Callback
}
);
The WS server can be reached on the same port as the Express application, and does not require a special path.
In order to easily lookup and process Express routes, the middleware requires that WS messages are sent in a standardized format:
interface ClientMessage {
type: 'route' | 'subscription';
message: ClientRouteMessage|ClientSubscriptionMessage
}
interface ClientRouteMessage
{
route: string;
method: 'get'|'post'|'put'|'delete'|'patch';
body?: any;
}
interface ClientSubscriptionMessage
{
eventName: string;
subscriptionId?: string;
}
You may make standard route requests - such as get, post, put, etc - to Express
endpoints and receive the response that would be returned in an HTTP request. To
make these requests, use the ClientRouteMessage
schema:
The path to request. This should be the same path that you would request from
your HTTP server. For example, if a status page is located at /status
, then
the route
should be /status
.
The method the request is made with. This is purely organizational, since websockets do not use HTTP methods. This is used to look up the correct Express route to call, since there may be multiple methods associated with the same path, each with their own set of logic.
{
"type": "route",
"message": {
"method": "POST",
"route": "/",
"body": {
"key": "value"
}
}
}
A websocket client may wish to subscribe to backend events and have data pushed
from the API, rather than the classic "pull via request" model of HTTP requests and
the Standard Route Requests. To make these requests, use the ClientSubscriptionMessage
schema:
The name of the event to subscribe to. This will depend on your implementation and what your application may broadcast.
Optional. You may wish to further scope the subscription to an "ID". This will
just be used to fetch the subscription when data is broadcast on the associated
eventName
. This may be used if you don't want to get all events for a given
name, but only want to receive events with a certain subscriptionId
attached.
{
"type": "subscription",
"message": {
"eventName": "some-event",
"subscriptionId": "optional-id"
}
}
Responses are also in a standard format:
interface ClientResponse
{
request: ClientMessage;
response: {
status: number;
body?: any;
},
}
This is a copy of the original message sent by the client.
The status code that was set by the Express route.
The body that was sent by the Express route. This attribute may be undefined
.
The selling point of websockets is their bidirectional nature. Emulating the basic HTTP request lifecycle - request and response - only goes so far, and the true power of websockets lies in the ability to broadcast to connected clients.
WsExpress provides a broadcast
function that will send a message to all
subscribed clients.
import {broadcast} from "wsexpress";
broadcast(
'eventName',
{}, // Payload to send
'subscriptionId' // Optional
);
The payload is sent to all clients subscribed to the provided event name, and optional subscription ID. The payload will be sent in the standard response format:
{
"request": {}, // The original subscription request
"response": {
"status": 200, // Will always be 200
"body": {} // The payload that was broadcasted
}
}
In this case the request
that is returned will always be the original subscription
request - that is, the request that the client sent to add the subscription in the
first place.
If your API runs in a multi-instance environment, you will have issues with broadcasting.
The reason for this is that websocket clients are connected to only one server
at a time. The ConnectionManager
which is responsible for handling broadcasts will
only have a list of clients that are connected to its specific server - it has
no knowledge of the clients connected to other servers in a cluster for example.
Therefore, you should attempt to execute broadcast
on all servers in a cluster
so that all connected clients will receive the event payload. This can be done in
many ways, one of which being queued jobs.
There are multiple debug
scenarios:
wsexpress-http
- logs requests to the intermediate HTTP request handlerwsexpress-server
- logs upgrade requests and route handling eventswsexpress-client
- logs client message eventswsexpress-connections
- logs client connect/disconnect events
DEBUG=wsexpress-* node .
DEBUG=wsexpress-http,wsexpress-server node .
yarn test
yarn lint
yarn lint:fix # Fixes lint errors