- Introduction
- Getting started
- Philosophy
- Comparison
- Limitations
- Debugging runbook
- FAQ
- Basics
- Concepts
- Network behavior
- Integrations
- API
- CLI
- Best practices
- Recipes
- Cookies
- Query parameters
- Response patching
- Polling
- Streaming
- Network errors
- File uploads
- Responding with binary
- Custom worker script location
- Global response delay
- GraphQL query batching
- Higher-order resolver
- Keeping mocks in sync
- Merging Service Workers
- Mock GraphQL schema
- Using CDN
- Using custom "homepage" property
- Using local HTTPS
1.x → 2.x
Migration guidelines for version 2.0.
About the release
Version 2.0 brings the biggest API change to the library since its inception. Alongside the new API, it includes various features, such as ReadableStream
support, ESM-compatibility, and countless bug fixes. This guide will help you migrate your application to version 2.0. We highly recommend you read it from start to finish.
Make sure to read the official announcement for this release if you’ve missed it!
Introducing MSW 2.0
Official announcement post.
Codemods
Our friends at Codemod.com have prepared a fantastic collection of codemods that can help you migrate to MSW 2.0.
Installation
npm install msw@latest
Breaking changes
Environment
Node.js version
This release sets the minimal supported Node.js version to 18.0.0.
Node.js versions prior to Node.js 18 are no longer supported. To use the next versions of MSW, please migrate to Node.js 18 or higher.
TypeScript version
This release sets the minimal supported TypeScript version to 4.7. If you are using an older TypeScript version, please migrate to version 4.7 or later to use MSW. Please consider that at the moment of writing this TypeScript 4.6 is almost two years old.
Imports
Worker imports
Everything related to the browser-side integration is now exported from the msw/browser
entrypoint. This includes both the setupWorker
function and the relevant type definitions.
Before:
import { setupWorker } from 'msw'
After:
import { setupWorker } from 'msw/browser'
Response resolver arguments
Response resolver function no longer accepts req
, res
, and ctx
arguments. Instead, it accepts a single argument which is an object containing information about the intercepted request.
Before:
rest.get('/resource', (req, res, ctx) => {})
After:
http.get('/resource', (info) => {})
Depending on the handler namespace used (http
or graphql
), the info
object contains different properties. You can learn about how to access request information now in the Request changes.
Learn more about the updated call signature of the request handler namespaces:
http
API reference for the `http` namespace.
graphql
API reference for the `graphql` namespace.
Request changes
Request URL
Since the intercepted request is now described as a Fetch API Request
instance, its request.url
property is no longer a URL
instance but a plain string.
Before:
rest.get('/resource', (req) => {
const productId = req.url.searchParams.get('id')
})
After:
If you wish to operate with it as a URL
instance, you should create it first from the request.url
string.
import { http } from 'msw'
http.get('/resource', ({ request }) => {
const url = new URL(request.url)
const productId = url.searchParams.get('id')
})
Request params
Path parameters are no longer exposed under req.params
.
Before:
rest.get('/post/:id', (req) => {
const { id } = req.params
})
After:
To access path parameters, use the params
object on the response resolver.
import { http } from 'msw'
http.get('/post/:id', ({ params }) => {
const { id } = params
})
Request cookies
Request cookies are no longer exposed under req.cookies.
Before:
rest.get('/resource', (req) => {
const { token } = req.cookies
})
After:
To access request cookies, use the cookies
object on the response resolver.
import { http } from 'msw'
http.get('/resource', ({ cookies }) => {
const { token } = cookies
})
Request body
You can no longer read the intercepted request body via the req.body
property. In fact, according to the Fetch API specification, request.body
will now return a ReadableStream
if the body is set.
Before:
rest.post('/resource', (req) => {
// The library would assume a JSON request body
// based on the request's "Content-Type" header.
const { id } = req.body
})
After:
MSW will no longer assume the request body type. Instead, you should read the request body as you wish using the standard Request
methods like .text()
, .json()
, .arrayBuffer()
, etc.
import { http } from 'msw'
http.post('/user', async ({ request }) => {
// Read the request body as JSON.
const user = await request.json()
const { id } = user
})
Response declaration
Mocked responses are no longer declared using the res()
composition function. We are departing from the composition approach in favor of adhering to the web standards.
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
After:
To declare a mocked response, create a Fetch API Response
instance and return it from the response resolver.
import { http } from 'msw'
http.get('/resource', () => {
return new Response(JSON.stringify({ id: 'abc-123' }), {
headers: {
'Content-Type': 'application/json',
},
})
})
To provide a less verbose interface and also support such features as mocking response cookies, the library now provides a custom HttpResponse
class that you can use as a drop-in replacement for the native Response
class.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
}),
]
Learn more about the new HttpResponse
API:
HttpResponse
API reference for the `HttpResponse` class.
req.passthrough()
Before:
rest.get('/resource', (req, res, ctx) => {
return req.passthrough()
})
After:
import { http, passthrough } from 'msw'
export const handlers = [
http.get('/resource', () => {
return passthrough()
}),
]
res.once()
Since the res()
composition API is gone, so is the res.once()
one-time request handler declaration.
Before:
rest.get('/resource', (req, res, ctx) => {
return res.once(ctx.text('Hello world!'))
})
After:
To declare a one-time request handler, provide an object as the third argument to it, and set the once
property of that object to true
.
import { http, HttpResponse } from 'msw'
http.get(
'/resource',
() => {
return new HttpResponse('Hello world!')
},
{ once: true }
)
res.networkError()
To mock a network error, call the HttpResponse.error()
static method and return it from the response resolver.
Before:
rest.get('/resource', (req, res, ctx) => {
return res.networkError('Custom error message')
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.error()
})
Note that the
Response.error()
doesn’t accept a custom error message. Previously, MSW did its best to coerce the custom error message you provided to the underlying request client but it never worked reliably because it’s up to the request client to handle or disregard the network error message.
Context utilities
With this release we are deprecating the ctx
utilities object. Instead, use the HttpResponse
class to declare mocked response properties, like status, headers, or body.
ctx.status()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.status(201))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
status: 201,
})
})
ctx.set()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.set('X-Custom-Header', 'foo'))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'X-Custom-Header': 'foo',
},
})
})
Learn about the standard
Headers
API.
ctx.cookie()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.cookie('token', 'abc-123'))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse(null, {
headers: {
'Set-Cookie': 'token=abc-123',
},
})
})
The library is able to detect whenever you are mocking response cookies via the HttpResponse
class. If you wish to mock response cookies, you must use that class, since response cookies cannot be read on the native Response
class after they are set.
ctx.body()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.body('Hello world'), ctx.set('Content-Type', 'text/plain'))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', (req, res, ctx) => {
return new HttpResponse('Hello world')
})
ctx.text()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.text('Hello world!'))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return new HttpResponse('Hello world!')
})
ctx.json()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.json({ id: 'abc-123' }))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.json({ id: 'abc-123' })
})
Note that you don’t have to explicitly specify the
Content-Type
response header when using staticHttpResponse
methods likeHttpResponse.text()
,HttpResponse.json()
, and others.
ctx.xml()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.xml('<foo>bar</foo>'))
})
After:
import { http, HttpResponse } from 'msw'
http.get('/resource', () => {
return HttpResponse.xml('<foo>bar</foo>')
})
ctx.data()
Before:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
})
)
})
After:
The graphql
handler namespace no longer gets a special treatment. Instead, you should declare standard JSON responses directly.
To make the mocked response definition for GraphQL operations more comfortable, use the HttpResponse.json()
static method:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
})
})
Using
HttpResponse
, you have to explicitly include the root-leveldata
property on the response.
ctx.errors()
Before:
graphql.mutation('Login', (req, res, ctx) => {
const { username } = req.variables
return res(
ctx.errors([
{
message: `Failed to login: user "${username}" does not exist`,
},
])
)
})
After:
import { graphql, HttpResponse } from 'msw'
graphql.mutation('Login', ({ variables }) => {
const { username } = variables
return HttpResponse.json({
errors: [
{
message: `Failed to login: user "${username}" does not exist`,
},
],
})
})
Using
HttpResponse
, you have to explicitly include theerrors
root-level property on the response.
ctx.extensions()
Before:
graphql.query('GetUser', (req, res, ctx) => {
return res(
ctx.data({
user: {
firstName: 'John',
},
}),
ctx.extensions({
requestId: 'abc-123',
})
)
})
After:
import { graphql, HttpResponse } from 'msw'
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
extensions: {
requestId: 'abc-123',
},
})
})
ctx.delay()
Before:
rest.get('/resource', (req, res, ctx) => {
return res(ctx.delay(500), ctx.text('Hello world'))
})
After:
The library now exports the delay()
function that returns a timeout Promise
. You can await it anywhere in your response resolvers to emulate server-side delay.
import { http, HttpResponse, delay } from 'msw'
http.get('/resource', async () => {
await delay(500)
return HttpResponse.text('Hello world')
})
The call signature of the delay()
function remains identical to the previous ctx.delay()
.
delay
API reference for the `delay` function.
ctx.fetch()
Before:
rest.get('/resource', async (req, res, ctx) => {
const originalResponse = await ctx.fetch(req)
const originalJson = await originalResponse.json()
return res(
ctx.json({
...originalJson,
mocked: true,
})
)
})
After:
To perform an additional request within the handler, use the new bypass
function exported from msw
. This function wraps any given Request
instance, marking it as the one MSW should ignore when intercepting requests.
import { http, HttpResponse, bypass } from 'msw'
http.get('/resource', async ({ request }) => {
const originalResponse = await fetch(bypass(request))
const originalJson = await originalResponse.json()
return HttpResponse.json({
...originalJson,
mocked: true,
})
})
bypass
API reference for the `bypass` function.
printHandlers()
The .printHandlers()
method on worker
/server
has been removed in favor of the new .listHandlers()
method.
Before:
worker.printHandlers()
After:
The new .listHandlers()
method returns a read-only array of currently active request handlers.
worker.listHandlers().forEach((handler) => {
console.log(handler.info.header)
})
onUnhandledRequest
The request
argument of the onUnhandledRequest
has changed from being an abstract request object to be a Fetch API Request
instance. Take that into account when accessing its properties, like request.url
.
Before:
server.listen({
onUnhandledRequest(request, print) {
const url = request.url
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
After:
The request
argument is an instance of Request
, which makes its url
property a string
.
server.listen({
onUnhandledRequest(request, print) {
// Create a new URL instance manually.
const url = new URL(request.url)
if (url.pathname.includes('/assets/')) {
return
}
print.warning()
},
})
Life-cycle events
This release brings changes to the Life-cycle events listeners’ call signature.
Before:
server.events.on('request:start', (request, requestId) => {})
After:
Every life-cycle event listener now accepts a single argument being an object.
server.events.on('request:start', ({ request, requestId }) => {})
New API
In addition to the breaking changes, this release introduces a list of new APIs. Most of them are focused on providing compatibility with the deprecated functionality.
Frequent issues
Request
/Response
/TextEncoder
is not defined (Jest)
Make sure you are using Node.js v18 or newer before reading further.
This issue is caused by your environment not having the Node.js globals for one reason or another. This commonly happens in Jest because it intentionally robs you of Node.js globals and fails to re-add them in their entirely. As the result, you have to explicitly add them yourself.
Create a jest.polyfills.js
file next to your jest.config.js
with the following content:
// jest.polyfills.js
/**
* @note The block below contains polyfills for Node.js globals
* required for Jest to function when running JSDOM tests.
* These HAVE to be require's and HAVE to be in this exact
* order, since "undici" depends on the "TextEncoder" global API.
*
* Consider migrating to a more modern test runner if
* you don't want to deal with this.
*/
const { TextDecoder, TextEncoder } = require('node:util')
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
})
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Blob: { value: Blob },
File: { value: File },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
})
Make sure to install
undici
. It’s the official fetch implementation in Node.js.
Then, set the setupFiles
option in jest.config.js
to point to the newly created jest.polyfills.js
:
// jest.config.js
module.exports = {
setupFiles: ['./jest.polyfills.js'],
}
Pay attention it’s the setupFiles
option, and not setupFilesAfterEnv
.
The missing Node.js globals must be injected before the environment (e.g.
JSDOM).
If you find this setup cumbersome, consider migrating to a modern testing framework, like Vitest, which has none of the Node.js globals issues and provides native ESM support out of the box.
Cannot find module ‘msw/node’ (JSDOM)
This error is thrown by your test runner because JSDOM uses the browser
export condition by default. This means that when you import any third-party packages, like MSW, JSDOM forces its browser
export to be used as the entrypoint. This is incorrect and dangerous because JSDOM still runs in Node.js and cannot guarantee full browser compatibility by design.
To fix this, set the testEnvironmentOptions.customExportConditions
option in your jest.config.js
to ['']
:
// jest.config.js
module.exports = {
testEnvironmentOptions: {
customExportConditions: [''],
},
}
This will force JSDOM to use the default export condition when importing msw/node
, resulting in correct imports.
multipart/form-data is not supported
Error in Node.js
Earlier versions of Node.js, like v18.8.0, didn’t have official support for request.formData()
. Please upgrade to the latest Node.js 18.x where such a support has been added.