Describing WebSocket API

Learn how to describe WebSocket API with Mock Service Worker.

Import

MSW provides a designated ws namespace for describing WebSocket events. We will use that namespace to describe what connections and events to intercept and how to handle them.

Import the ws namespace from the msw package:
// src/mocks/handlers.js
import { ws } from 'msw'
 
export const handlers = []

Event handler

WebSocket communications are event-based so we will be using an event handler to intercept and describe them.

In this tutorial, we will describe a chat application that uses WebSocket to send and receive messages. You can imagine that application like this:

// src/app.js
const ws = new WebSocket('wss://chat.example.com')
 
// Handle receiving messages.
ws.addEventListener('message', (event) => {
  renderMessage(event.data)
})
 
// Handle sending messages.
const handleFormSubmit = (event) => {
  const data = new FormData(event.target)
  const message = data.get('message')
  ws.send(message)
}

Let’s start by creating an event handler for a WebSocket endpoint using the ws.link() method.

Call ws.link() to declare your first event handler:
// src/mocks/handlers.js
import { ws } from 'msw'
 
const chat = ws.link('wss://chat.example.com')
 
export const handlers = [
  chat.on('connection', ({ client }) => {
    console.log('Intercepted a WebSocket connection:', client.url)
  }),
]

The chat object returned from the ws.link() method gives us the server-like API to interact with the intercepted WebSocket connection. We can add the "connection" event listener to know when a client in our application tries to connect to the specified WebSocket server.

Next, let’s describe the incoming and outgoing events for the WebSocket connection.

Event flow

WebSocket communications are duplex, which means that the client may receive events it hasn’t explicitly requested. With that in mind, the WebSocket event handlers you create sit in-between the client and the server, allowing you to intercept and mock both client-to-server and server-to-client events.

client ⇄ MSW ⇄ server

This means that the event handler may both act as a replacement for a WebSocket Server (e.g. when developing mock-first) as well as middleware layer that proxies, observes, or modifies the actual client-to-server communication. We will take a look at both scenarios in this tutorial.

Client events

Intercepting client events

Any event sent by the WebSocket client is considered an outgoing event. To intercept an outgoing client event, add a "message" event listener on the client object provided to you by the event handler.

Add a "message" listener to the client to intercept client events:

chat.on('connection', ({ client }) => {
  client.addEventListener('message', (event) => {
    console.log('Intercepted an outgoing message:', event.data)
  })
})

The event handler is compliant with the WHATWG WebSocket specification, which means it exposes messages as MessageEvent instances.

Mocking client events

To send a server-to-client event, the client object provides a send() method that can send text, Blob, and ArrayBuffer data to the client.

Use client.send() to mock an incoming client event:
chat.on('connection', ({ client }) => {
  client.addEventListener('message', (event) => {
    client.send('hello from server')
  })
})

With this event listener, every outgoing client event (us sending a message to the chat) will receive a "hello from server" message from the “server”.

Note that you can call client.send() anywhere in the connection listener. That way, you can send server-to-client data outside of the client message handling logic.

chat.on('connection', ({ client }) => {
  // Immediately send this message to every
  // connected WebSocket client.
  client.send('hello from server')
})

Broadcasting client events

The client.send() method sends data to the individual connected WebSocket client. In order to broadcast data to multiple clients, MSW provides a broadcast() and broadcastExcept() methods on the event handler.

For example, we can broadcast a message to everyone whenever a new client joins the chat, including that client:

chat.on('connection', ({ client }) => {
  // Broadcast this message to all connected clients.
  chat.broadcast('all say hi to a new client')
})

We will use the broadcastExcept() method to broadcast a client-sent message to all other clients so they can see it in the chat too.

chat.on('connection', ({ client }) => {
  // Whenever a client sends a message...
  client.addEventListener('message', (event) => {
    // ...broadcast it to all other clients.
    chat.broadcastExcept(client, event.data)
  })
})

Server events

Connecting to server

By default, MSW does not establish the actual WebSocket server connection. This is handy when prototyping and developing mock-first.

In order to affect the server-to-client communication, you must establish the actual server connection by calling server.connect() within the connection listener.

Call server.connect() to establish the actual WebSocket server connection:

chat.on('connection', ({ client, server }) => {
  server.connect()
})

This will connect the WebSocket client to the actual server and establish the server-to-client communication.

Forwarding client events

Even with the server connection established, no client events will be forwarded to that server by default. This gives the client-to-server messaging an opt-in nature: no outgoing events are forwarded and you can decide which are.

To enable client-to-server event forwarding, listen to the client messages you wish to forward and use server.send() method to send events to the actual server:

chat.on('connection', ({ client, server }) => {
  server.connect()
 
  // Listen to all messages the client sends...
  client.addEventListener('message', (event) => {
    // ...and send (forward) them to the server.
    server.send(event.data)
  })
})

Intercepting server events

You can intercept the events sent from the actual server by adding a "message" listener to the server object.

chat.on('connection', ({ client, server }) => {
  server.addEventListener('message', (event) => {
    console.log('Intercepted an incoming message:', event.data)
  })
})

This means you don’t have to call client.send() to forward an intercepted server-sent event to the client—it will be forwarded automatically. This allows MSW to keep a transparent server-to-client communication, giving you the means to modify it when needed.

Modifying server events

You can modify the server-sent event before it reaches the WebSocket client by preventing it first, and then using client.send() to send whichever modified data you wish.

Use event.preventDefault() to prevent server-to-client forwarding, and client.send() to send a mock data:

chat.on('connection', ({ client, server }) => {
  server.addEventListener('message', (event) => {
    if (event.data === 'hello from server') {
      // Prevent this particular event from being
      // forwarded to the client.
      event.preventDefault()
 
      // Send a mocked data to the client instead.
      client.send(event.data.replace('server', 'mock'))
    }
  })
})

Since the default server-sent message behavior is to forward that message to the client, by calling event.preventDefault(), you opt-out from that behavior.

In the scenario above, whenever the actual server sends a "hello from server" event, it will be intercepted, prevented, and a mocked "hello from mock" event will be sent to the client instead.

Mocking server events

To mock a client-to-server event, the server object provides a send() method similar to that of the client object.

Use server.send() to mock an outgoing client event:
chat.on('connection', ({ client, server }) => {
  server.connect()
 
  client.addEventListener('message', (event) => {
    server.send(event.data)
  })
 
  server.addEventListener('message', (event) => {
    if (event.data === 'ping') {
      server.send('pong')
    }
  })
})

Here, whenever the actual server sends a "ping" message, we immediately send a mocked "pong" message from the client to the server. Since the server event is not prevented, it will be forwarded to the client as well.