boundary()

Scope the network interception to the given boundary.

Call signature

The server.boundary() function accepts a callback function and returns a new function with the same call signature as the given callback but bound.

function boundary<Callback extends (...args: Array<unknown>) => unknown>(
  callback: Callback
): (...args: Parameters<Callback>) => ReturnType<Callback>

Usage

The server.boundary() API is designed to provide the network behavior isolation. Any modifications to the request interception made within a boundary will only affect that boundary and nothing else.

import { HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer()
server.listen()
 
function app() {
  fetch('https://example.com')
 
  server.boundary(() => {
    // This runtime handler override will only affect
    // the network within this server boundary.
    server.use(
      http.get('https://example.com', () => {
        return HttpResponse.error()
      })
    )
 
    fetch('https://example.com')
  })()
}

In the example above, the first fetch() call will be handled by whichever request handlers initially provided to the setupServer() call, which in this case is none. The same fetch() call within the boundary, however, will receive a network error (HttpResponse.error()) because the respective request handler override was added within the boundary.

Since the boundary provides scope isolation, you don’t need to reset the request handlers. You do need to reset them, however, if you wish to reset the network behavior within that server boundary.

The server.boundary() API utilizes AsyncLocalStorage, which means that all the network in the current scope and child scopes of the boundary will be affected by request handler overrides.

The server boundary accepts whichever arguments passed to the callback function and returns whatever that function returns. With that in mind, you can use it even in situations when the callback is expected to return something.

// Let's imagine you are using a server boundary
// in a backend framework that expects route handlers
// to return Fetch API Response instances.
router.get(
  '/resource',
  // This boundary will accept whatever arguments
  // were given to it by "router.get", and return a
  // new Response instance to the router.
  server.boundary((request) => {
    return new Response('Hello world')
  })
)

Standalone

This API can be used standalone for various purposes, like a scoped network introspection, debugging, and development. In the example below, the server.boundary() is used to introspect all the network requests that are happening as a part of the POST /resource route handling in Express.

import express from 'express'
import { http } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer()
server.listen()
 
const app = express()
 
app.post('/resource', (req, res) => {
  server.boundary(() => {
    server.use(
      http.all('*', ({ request }) => {
        console.log(request.method, request.url)
      })
    )
 
    handleRequest(req, res)
  })()
})

Concurrent test runs

The server.boundary() API is primarily designed to support concurrent test runs in modern test frameworks. Since the total list of request handlers is kept in-memory in the setupServer() scope, this introduces a global state problem. If multiple concurrent tests call server.use(), those request handler override will end up affecting irrelevant tests that run in parallel.

Introducing a server boundary in each test solves this problem and prevents request handler overrides from ever affecting irrelevant tests. Take a look at how server.boundary() is used in practice in this concurrent test suite in Vitest:

import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
beforeAll(() => {
  server.listen()
})
 
afterAll(() => {
  server.close()
})
 
it.concurrent(
  'fetches the user',
  server.boundary(async () => {
    // This test doesn't introduce any request handlers override.
    // The network within this test will be resolved against the
    // initial request handlers provided to "setupServer()" call.
    const response = await fetch('https://example.com/user')
    const user = await response.json()
    expect(user).toEqual({ name: 'John' })
  })
)
 
it.concurrent(
  'handles the server error',
  server.boundary(async () => {
    // This test makes the user requests return a 500 response.
    // Fetching the user in this scope, and any nested scopes,
    // will always result in a 500 response.
    server.use(
      http.get('https://example.com/user', () => {
        return new HttpResponse(null, { status: 500 })
      })
    )
    const response = await fetch('https://example.com/user')
    expect(response.status).toBe(500)
  })
)
 
it.concurrent(
  'handles network errors',
  server.boundary(async () => {
    // This test makes the user requests fail with a network error.
    server.use(
      http.get('https://example.com/user', () => {
        return HttpResponse.error()
      })
    )
 
    await expect(fetch('https://example.com/user')).rejects.toThrow(
      'Failed to fetch'
    )
  })
)

Although each test case relies on a particular network state, using the server.boundary() API allows to scope and “freeze” that state, resulting in predictable network behavior in concurrent test runs.

Nested boundaries

Whenever a server boundary is created, it treats whichever existing request handlers from the higher scope as the initial request handlers.

const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
server.boundary(async () => {
  // The user request will return a 200 JSON response
  // as described in the initial request handlers
  // provided to the "setupServer" call above.
  await fetch('https://example.com/user')
})()

Any request handler overrides within the boundary are prepended to the initial list of request handlers, similar to how they are in the regular .use() usage.

When a server boundary is nested within another server boundary, whichever request handler state the upper boundary has is treated as the initial state for the nested boundary.

const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
// This server boundary has the following request handlers:
// - (initial) GET /user -> 200 OK
// - (override) POST /login -> 500 Internal Server Error
server.boundary(() => {
  server.use(
    http.post('https://example.com/login', () => {
      return new HttpResponse(null, { status: 500 })
    })
  )
 
  // This server boundary has the following request handlers:
  // - (initial) GET /user -> 200 OK
  // - (initial) POST /login -> 500 Internal Server Error
  // - (override) DELETE /post -> 404 Not Found
  server.boundary(() => {
    server.use(
      http.delete('https://example.com/post', () => {
        return new HttpResponse(null, { status: 404 })
      })
    )
 
    // Resetting the request handlers will remove any
    // request handler overrides added in *this* boundary.
    // The resulting request handlers will be:
    // - (initial) GET /user -> 200 OK
    // - (initial) POST /login -> 500 Internal Server Error
    server.resetHandlers()
  })()
})()