Custom request handler
Use case
Creating a custom request handler is not something you would do most of the times when working with the library. However, there are cases when the usage of the standard request handlers (rest.*
/graphql.*
) is insufficient or inapplicable. A vibrant example of such use case is API mocking for an SDK.
When using an SDK you are provided with an interface that wraps request calls in a more domain-driven way. For example:
1// A random "externalSDK" package2// that allows us to list some resources.3externalSDK.listResources()
With SDK being an abstraction over requests, the exact method and endpoint used under the hood become its implementation details. It's highly discouraged to base your testing on such implementation details, as they are subjected to change and are not exposed publicly for a reason.
Instead, you can target a request made by an SDK by implementing a custom request matching and resolving logic with MSW.
Request handler API
1interface RequestHandler<2 RequestType,3 ContextType,4 ParsedRequest,5 PublicRequest,6 ResponseBodyType7> {8 parse?(req: RequestType): ParsedRequest | null910 predicate(req: RequestType, parsedReq: ParsedRequest): boolean1112 getPublicRequest?(13 req: RequesType,14 parsedRequest: ParsedRequest,15 ): PublicRequest1617 resolver: ResponseResolver1819 defineContext?(req: PublicRequest): ContextType2021 log(22 req: PublicRequest,23 res: ResponseWithSerializedHeaders,24 handler: RequestHandler<25 RequestType,26 ContextType,27 ParsedRequest,28 PublicRequest,29 ResponseBodyType30 >,31 parsedRequest: ParsedRequest,32 ): void33}
parse
Parses a request, providing the result to the rest of the request handler's lifecycle methods.
Useful for deriving or normalizing request payload for the rest of the processing chain (i.e. getting an operation name, transforming body properties).
getPublicRequest
Maps the captured and parsed request data to the publicly exposed req
object in the response resolver.
Useful to expose certain parsed request information, while preserving the rest for the internal purposes. For example, you can parse GraphQL variables in the parse
method and expose them in the getPublicRequest
later, so they could be references under req.variables
in the response resolver:
1{2 parse(req) {3 // Everything returned from the `parse` function4 // is available only internally via the `parsedRequest` object.5 return {6 variables: parseVariables(req.body)7 }8 },9 getPublicRequest(req, parsedRequest) {10 // Everything returned from the `getPublicRequest` function11 // is available publicly under the `req` object12 // in the response resolver.13 return {14 ...req,15 variables: parsedRequest.variables16 }17 }18}
predicate
Determines whether the captured request must be responded with a mocked response.
resolver
Returns a mocked response for the matched request.
Read more in the response resolver section.
defineContext
Overrides the default context (ctx
) object available in the response resolver.
Useful to provide custom response transformers specific to the ...
1export const customHandler = (resolver) => {2 return {3 // ...predicate and other properties4 resolver,5 defineContext(req, parsedRequest) {6 return {7 resource(payload) {8 return (res) => {9 res.headers.set('content-type', 'application/json')10 res.body = {11 ...payload,12 resourceId: getRandomId(),13 }14 return res15 }16 },17 }18 },19 }20}
The example above would expose the ctx.resource()
function in the response resolver, allowing for a shorthand resource creation:
1customHandler((req, res, ctx) => {2 return res(ctx.resource({ title: 'Random resource' }))3})
You can utilize default contexts, such as
defaultContext
,restContext
andgraphqlContext
exposed from themsw
package.
log
Prints out the resolved request and its response to browser's console.
Create a handler
Getting back to our SDK example, let's say that the externalSDK.listResources()
produces the following request:
1GET /sdk/resources23Headers:4 Content-Type: "application/json"56Body:7 {8 "version": "1.5",9 "operation": "ListResources",10 "region": "ua"11 }
When creating a custom request handler it's crucial to define reliable data for the request matching logic. As the endpoint string is an implementation detail of the SDK, the operation
request body property is a description of the SDK method that's been invoked.
Let's create a request handler that would allow to capture requests based on their operation name:
1export const sdk = (operation, resolver) => {2 return {3 predicate(req) {4 // Match requests which "operation" body property5 // equals to the given "operation" string of the handler.6 const req.body.operation === operation7 },8 // Propagate the response resolver as-is.9 resolver,10 // Print out the matched request in browser's console.11 log(req, res) {12 console.log('Request', req)13 console.log('Response', res)14 }15 }16}
Note that you may want to narrow down the custom
predicate
function by utilizing other request data (headers, body payload) to reduce the chance of false positive matches.
Provide the custom sdk
request handler to the setupWorker
/setupServer
calls, just as you would do with the standard handlers.
1import { setupWorker } from 'msw'2import { sdk } from './handlers/sdk'34const worker = setupWorker(5 sdk('ListResources', (req, res, ctx) => {6 return res(ctx.json({ id: 1 }))7 }),8)910worker.start()