Express / WebSockets for Real-Time Updates

A quick primer on using Express and WebSockets to enable real-time updates in your app or website.
3 min read

This post assumes you have a good understanding of Express and web servers. If you don’t, I recommend reading up both ExpressJS and the ws module, as well as the MDN WebSocket guide.

If you already have express, you can add the ws module to your project with npm install ws --save.

What is a Websocket?

WebSockets are a bidirectional communication protocol that allows for real-time “push” updates from the server to a client and vice versa. This is in contrast to the traditional HTTP request/response model, where the client must poll the server for updates.

If implementing something like real-time commenting or chat, you can have all the connected clients listening for new messages and updating in real-time when a new message is sent.

Server Side Implementation

First, we need a server

Although the ws module can run on its own, it’s often easier to run it alongside an existing Express server. This way, you can serve your app and WebSocket connections from the same server.

At scale, it may be desirable to scale these independently, but at that point, you’ll likely have message brokers and other infrastructure in place to facilitate this.

import express from 'express'
import { WebSocketServer } from 'ws'

const app = express()
const server = app.listen(port, '0.0.0.0', () => {
  console.log(`Example app listening on port ${port}`)
})

// create a websocket server
const wsServer = new WebSocketServer({ noServer: true })

Upgrade Connections

We get WebSocket connections by “upgrading” the HTTP connection to a WebSocket connection. Express exposes this via the upgrade event.

server.on('upgrade', (request, socket, head) => {
  wsServer.handleUpgrade(request, socket, head, socket => {
    wsServer.emit('connection', socket, request)
  })
})

Accepting Connections

We can then listen for messages from the client like this:

// listen for events
wsServer.on('connection', socket => {
  socket.on('error', err => console.error('Websocket error:', err))

  socket.on('message', message => {
    // can store info in the socket object
    socket.userData = { userId: 12345 }  // TODO: example
  })

  // on initial connection, send an example message
  socket.send(JSON.stringify({ type: 'serverHello', message: 'hello' }))
})

Passing Along Events

For this example, you’ll need to use a little imagination. But, you can pass along events from your app to the WebSocket server like in the example below. You could be pulling from some message bus or other event source here.

// listen for an event
someEventBus.on('event', (eventData) => {
  // send a message to all clients that are connected
  wsServer.clients.forEach(client => {
    client.send(JSON.stringify({ type: 'event', message: eventData }))
  })
})

Connecting from the Client

MDN has a great guide on setting up a client in your browser code, but here’s a quick example. This also works in React Native, Electron, and other environments:

// for prod, use wss:// instead for secure connections to your server with TLS enabled
const ws = new WebSocket('ws://localhost:8080')

ws.onopen = () => {
  console.log('connected')

  // send a message to the server
  ws.send(JSON.stringify({ type: 'clientHello', message: 'hello' }))
}

ws.onmessage = event => {
  // log the message from the server
  console.log('received:', event.data)
}

ws.onclose = () => {
  // log when the connection is closed
  console.log('disconnected')
}

// later on,
// ws.close()

Event Brokers

As I alluded to earlier, once you scale beyond a single server, not all clients will be connected to the same instance of your WebSocket server. This will make broadcast messages to all clients more difficult.

If that happens, you’ll need some kind of message broker to send messages between instances. You could use something like Redis or RabbitMQ. Postgres also has a pub/sub feature that can be used for this, so if you already have a Postgres instance, that could be a good option to start out with.

One nice thing about using Postgres for your database is you can set up triggers for specific update/insert events, and use those to notify users via the WebSocket.

You may also reach a point where it makes sense to make your WebSocket server a standalone service to scale it independently, and have your Express server communicate with it via your message broker of choice.

Next Steps

There’s plenty of additional functionality to implement beyond this basic setup, including:

Change Log

  • 7/2/2024 - Initial Revision

Found a typo or technical problem? file an issue!