This guide will help you migrate from the latest version of MSW to the next
release that introduces a first-class support for Fetch API primitives to the library. This is a breaking change. In fact, this is the biggest change to our public API since the day the library was first published. Do not fret, however, as this is precisely why this document exists.
npm install msw@next --save-dev
To help you navigate, we've structured this guide on the feature basis. You can read it top-to-bottom, or you can jump to a particular feature you have trouble migrating from.
- Imports
- Response resolver (call signature change)
- Request changes
- req.params
- req.cookies
- req.passthrough
- res.once
- res.networkError
- Context utilities
- Life-cycle events
.printHandlers()
- Advanced
- What's new in this release?
- Common issues
The rest
request handler namespace has been renamed to http
.
-import { rest } from 'msw'
+import { http } from 'msw'
This affects the request handlers declaration as well:
import { http } from 'msw'
export const handlers = [
http.get('/resource', resolver),
http.post('/resource', resolver),
http.all('*', resolver),
]
The setupWorker
API, alongside any related type definitions, are no longer exported from the root of msw
. Instead, import them from msw/browser
:
-import { setupWorker } from 'msw'
+import { setupWorker } from 'msw/browser'
Note that the request handlers like
http
andgraphql
, as well as the utility functions likebypass
andpassthrough
must still be imported from the root-levelmsw
.
A response resolver now exposes a single object argument instead of (req, res, ctx)
. That argument represents resolver information and consists of properties that are always present for all handler types and extra properties specific to handler types.
request
, a Fetch APIRequest
instance representing a captured request.cookies
, a parsed cookies object based on the request cookies.
params
, an object of parsed path parameters.
query
, a GraphQL query string extracted from either URL search parameters or a POST request body.variables
, an object of GraphQL query variables.
To mock responses, you should now return a Fetch API Response
instance from the response resolver. You no longer need to compose a response via res()
, and all the context utilities have also been removed.
http.get('/greet/:name', ({ request, params }) => {
console.log('Captured %s %s', request.method, request.url)
return new Response(`hello, ${params.name}!`)
})
Now, a more complex example for both REST and GraphQL requests.
import { http, graphql } from 'msw'
export const handlers = [
http.put('/user/:id', async ({ request, params, cookies }) => {
// Read request body as you'd normally do with Fetch.
const payload = await request.json()
// Access path parameters like before.
const { id } = params
// Access cookies like before.
const { sessionId } = cookies
return new Response(null, { status: 201 })
}),
graphql.mutation('CreateUser', ({ request, query, variables }) => {
return new Response(
JSON.stringify({
data: {
user: {
id: 'abc-123',
firstName: variables.firstName,
},
},
}),
{
headers: {
'Content-Type': 'application/json',
},
},
)
}),
]
Since the returned request
is now an instance of Fetch API Request
, there are some changes to its properties.
The request.url
property is a string (previously, a URL
instance). If you wish to operate with it like a URL
, you need to construct it manually:
http.get('/product', ({ request }) => {
// For example, this is how you would access
// request search parameters now.
const url = new URL(request.url)
const productId = url.searchParams.get('id')
})
Path parameters are now exposed directly on the Resolver info object (previously, req.params
).
http.get('/resource', ({ params }) => {
console.log('Request path parameters:', params)
})
Request cookies are now exposed directly on the Resolver info object (previously, req.cookies
).
http.get('/resource', ({ cookies }) => {
console.log('Request cookies:', cookies)
})
The library now does no assumptions when reading the intercepted request's body (previously, req.body
). Instead, you are in charge to read the request body as you see appropriate.
Note that since the intercepted request is now represented by a Fetch API
Request
instance, itsrequest.body
property still exists but returns aReadableStream
.
For example, this is how you would read request body:
http.post('/resource', async ({ request }) => {
const data = await request.json()
// request.formData() / request.arrayBuffer() / etc.
})
Using the Fetch API Response
instance may get quite verbose. To give you more convenient means of declaring mocked responses while remaining specification compliant and compatible, the library now exports an HttpResponse
object. You can use that object to construct response instances faster.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/user', () => {
// This is synonymous to "ctx.json()":
// HttpResponse.json() stringifies the given body
// and sets the correct "Content-Type" response header
// to describe a JSON response body.
return HttpResponse.json({ firstName: 'John' })
}),
]
Read more on how to use
HttpResponse
to mock REST API and GraphQL API responses.
Although MSW now respects the Fetch API specification, the older versions of Node.js do not, so you can't construct a Response
instance because there is no such global class.
To account for this, the library exports a Response
class that you should use when declaring request handlers. Behind the hood, that response class is resolved to a compatible polyfill in Node.js; in the browser, it only aliases global.Response
without introducing additional behaviors.
import { http,Response } from 'msw'
setupServer(
http.get('/ping', () => {
return new Response('hello world)
})
)
Relying on a single universal Response
class will allow you to write request handlers that can run in both browser and Node.js environments.
To create a one-time request handler, pass it an object as the third argument with once: true
set:
import { HttpResponse, http } from 'msw'
export const handlers = [
http.get(
'/user',
() => {
return HttpResponse.text('hello')
},
{ once: true },
),
]
To respond to a request with a network error, use the HttpResponse.error()
static method:
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.error()
}),
]
Note that we are dropping support for custom network error messages to be more compliant with the standard
Response.error()
network errors, which don't support custom error messages.
import { http, passthrough } from 'msw'
export const handlers = [
http.get('/user', () => {
// Previously, "req.passthrough()".
return passthrough()
}),
]
Most of the context utilities you'd normally use via ctx.*
were removed. Instead, we encourage you to set respective properties directly on the response instance:
import { HttpResponse, http } from 'msw'
export const handlers = [
http.post('/user', () => {
// ctx.json()
return HttpResponse.json(
{ firstName: 'John' },
{
status: 201, // ctx.status()
headers: {
'X-Custom-Header': 'value', // ctx.set()
},
},
)
}),
]
Let's go through each previously existing context utility and see how to declare its analogue using the Response
class.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.text('hello', { status: 201 })
}),
]
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.text('hello', {
headers: {
'Content-Type': 'text/plain; charset=windows-1252',
},
})
}),
]
import { HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.text('hello', {
headers: {
'Set-Cookie': 'token=abc-123',
},
})
}),
]
When you provide an object as the ResponseInit.headers
value, you cannot specify multiple response cookies with the same name. Instead, to support multiple response cookies, provide a Headers
instance:
import { HttpResponse, http } from 'msw'
export const handlers = [
http.get('/resource', () => {
return new HttpResponse(null, {
headers: new Headers([
// Mock a multi-value response cookie header.
['Set-Cookie', 'sessionId=123'],
['Set-Cookie', 'gtm=en_US'],
]),
})
}),
]
This is applicable to any multi-value headers, really.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return new HttpResponse('any-body')
}),
]
Do not forget to set the
Content-Type
header that represents the mocked response's body type. If using common response body types, like text or json, see the respective migration instructions for those context utilities below.
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.text('hello')
}),
]
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({ firstName: 'John' })
}),
]
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.xml('<user id="abc-123" />')
}),
]
The ctx.data
utility has been removed in favor of constructing a mocked JSON response with the "data" property in it.
import { HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({
data: {
user: {
firstName: 'John',
},
},
})
}),
]
The ctx.errors
utility has been removed in favor of constructing a mocked JSON response with the "errors" property in it.
import { HttpResponse } from 'msw'
export const handlers = [
http.get('/resource', () => {
return HttpResponse.json({
errors: [
{
message: 'Something went wrong',
},
],
})
}),
]
import { http, HttpResponse, delay } from 'msw'
export const handlers = [
http.get('/resource', async () => {
await delay()
return HttpResponse.text('hello')
}),
]
The delay
function has the same call signature as the ctx.delay
context function. This means it supports the delay mode as an argument:
await delay(500)
await delay('infinite')
The ctx.fetch()
function has been removed in favor of the bypass()
function. You should now always perform a regular fetch()
call and wrap the request in the bypass()
function if you wish for it to ignore any otherwise matching request handlers.
import { http, HttpResponse, bypass } from 'msw'
export const handlers = [
http.get('/resource', async ({ request }) => {
// Use the regular "fetch" from your environment.
const originalResponse = await fetch(bypass(request))
const json = await originalResponse.json()
// ...handle the original response, maybe return a mocked one.
}),
]
The bypass()
function also accepts RequestInit
as the second argument to modify the bypassed request.
// Bypass the given "request" and modify its headers.
bypass(request, {
headers: {
'X-Modified-Header': 'true',
},
})
The life-cycle events listeners now accept a single argument being an object with contextual properties.
-server.events.on('request:start', (request, requestId) = {})
+server.events.on('request:start', ({ request, requestId}) => {})
The request and response instances exposed in the life-cycle API have also been updated to return Fetch API Request
and Response
respectively.
The request ID is now exposed as a standalone argument (previously, req.id
).
server.events.on('request:start', ({ request, requestId }) => {
console.log(request.method, request.url)
})
To read a request body, make sure to clone the request first. Otherwise, it won't be performed as it would be already read.
server.events.on('request:match', async ({ request }) => {
// Make sure to clone the request so it could be
// processed further down the line.
const clone = request.clone()
const json = await clone.json()
console.log('Performed request with body:', json)
})
The response:*
events now always contain the response reference, the related request, and its id in the listener arguments.
worker.events.on('response:mocked', ({ response, request, requestId }) => {
console.log('response to %s %s is:', request.method, request.url, response)
})
The worker.prinHandlers()
and server.printHandlers()
methods were removed. Use the .listHandlers()
method instead:
-worker.printHandlers()
+console.log(worker.listHandlers())
It is still possible to create custom handlers and resolvers, just make sure to account for the new resolver call signature.
As this release removes the concept of response composition via res()
, you can no longer compose context utilities or abstract their partial composed state to a helper function.
Instead, you can abstract a common response logic into a plain function that creates a new Response
or modifies a provided instance.
// utils.js
import { HttpResponse } from 'msw'
export function augmentResponse(json) {
const response = HttpResponse.json(json, {
// Come up with some reusable defaults here.
})
return response
}
import { http } from 'msw'
import { augmentResponse } from './utils'
export const handlers = [
http.get('/user', () => {
return augmentResponse({ id: 1 })
}),
]
The main benefit of this release is the adoption of Fetch API primitives—Request
and Response
classes. By handling requests and responses as the platform does it, you bring your API mocking setup to the next level. Less library-specific abstractions, flatter learning curve, improved compatibility with other tools. But, most importantly, specification compliance and investment into a solution that uses standard APIs that are here to stay.
You can now read the intercepted request body as you would a regular Request
instance. This mainly means the addition of the following methods on the request
:
request.blob()
request.formData()
request.arrayBuffer()
For example, this is how you would read the request as Blob
:
import { http } from 'msw'
export const handlers = [
http.get('/resource', async ({ request }) => {
const blob = await request.blob()
}),
]
You can now send a ReadableStream
as the mocked response body. This is great for mocking any kind of streaming in HTTP responses.
import { http, HttpResponse, delay } from 'msw'
http.get('/greeting', () => {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode('hello'))
await delay(100)
controller.enqueue(encoder.encode('world'))
await delay(100)
controller.close()
},
})
return new HttpResponse(stream)
})
This likely means that you are running an old version of Node.js. Please use Node.js v18.14.0 and higher with this version of MSW. Also, see this.
Earlier versions of Node.js 18, like v18.8.0, had no support for request.formData()
. Please upgrade to the latest Node.js version where Undici have added the said support to resolve the issue.