skip to content

In Mock Service Worker (MSW) v2 a request handler is written as http.get('/api/users', resolver). What are the two halves of that handler, what must the resolver return, and does the component's own fetch() code have to change?

level: juniorimportance: must knowfreq 55%

answer

  1. two halves: match, then answer
  2. method plus path is the predicate
  3. second argument is a function
  4. it must hand back a Response
  5. HttpResponse.json builds one

basics

~20 s

An MSW handler pairs a predicate (method plus path, e.g. http.get('/api/users')) with a resolver function that returns a Response, usually built with HttpResponse.json(). Application code is untouched — MSW answers the real request at the network boundary.

solid answer

~40 s

A handler has two halves. The predicate is the call itself — `http.get('/api/users')` — which says *which* requests this handler claims: an HTTP method plus a path pattern that may contain params such as `/api/users/:id`. The second argument is the resolver, a function MSW calls when a request matches; it receives the intercepted request (and any path params) and must return a `Response`. In MSW v2 you build that with the `HttpResponse` helper, typically `HttpResponse.json(fixture)`, optionally with a status or headers. Nothing in the component changes: it still calls `fetch()` or its usual HTTP client against the same URL, and MSW resolves the request after it has left the application code. Handlers are plain values, so they live in a shared module and get passed into whichever setup the environment needs.

code

javascript · 11 lines
javascript
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([{ id: 1, name: 'Ada' }])
  }),

  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: Number(params.id), name: 'Ada' })
  }),
]

go deeper

for a junior

Be able to write one handler out loud: a method-and-path call, then a function that returns HttpResponse.json(fixture). Say plainly that the component's own fetch code is not modified.

for a middle

Explain the matching mechanics — method, path patterns with :params, absolute versus relative URLs, and first-match-wins ordering — and why a Response object rather than a fixture literal is required.

for a senior

Show how you organise handlers so a suite stays maintainable: a shared happy-path array per endpoint, per-test deviations only, and how you debug the four failure modes that all look like 'the mock did not work'.

for a principal

Frame handlers as an executable description of the API contract the frontend depends on, and be ready to argue who owns that description and how it is kept honest as the backend evolves.

## The shape of a handler A Mock Service Worker handler is a small declarative object created by a factory. In MSW v2 the factories live in two namespaces: `http` for REST-style requests (`http.get`, `http.post`, `http.put`, `http.patch`, `http.delete`, and `http.all` for any method) and `graphql` for GraphQL operations. Every factory call takes a predicate and a resolver: ```js import { http, HttpResponse } from 'msw' export const handlers = [ http.get('/api/users', () => { return HttpResponse.json([{ id: 1, name: 'Ada' }]) }), ] ``` The first argument — method plus path — is the predicate. The second is the resolver. ## The predicate half The predicate decides *whether this handler claims the request*. `http.get('/api/users')` claims only GET requests whose URL matches that path; a POST to the same path falls through to another handler or to the unhandled-request policy. Paths support named parameters, written with a colon: `http.get('/api/users/:id')` matches `/api/users/7` and hands the resolver `params.id === '7'` (always a string — parse it if you need a number). You can also write a full absolute URL, which is what you want when the app talks to a different origin, e.g. `http.get('https://api.example.com/users', …)`. Matching is per handler, in order: MSW walks the handler list and the first handler whose predicate matches resolves the request. That means a broad pattern placed first can shadow a specific one placed later, which is one of the more common self-inflicted bugs when a mock "doesn't work". ## The resolver half The resolver is an ordinary function — it may be async — that MSW invokes with an object describing the intercepted request. The fields you reach for most are `request` (a standard `Request`, so `await request.json()` gives you the posted body and `new URL(request.url).searchParams` gives you the query string) and `params` (the named path parameters). Its return value must be a `Response`. MSW v2 ships the `HttpResponse` helper for building one without boilerplate: ```js http.post('/api/orders', async ({ request }) => { const body = await request.json() return HttpResponse.json({ id: 1, ...body }, { status: 201 }) }) ``` `HttpResponse.json(value, init)` sets the JSON content type and serialises for you; there are also `HttpResponse.text()`, `HttpResponse.xml()` and the plain `new HttpResponse(body, init)` form. Returning a bare JavaScript object does **not** work — this is the single biggest v1-to-v2 trap, because MSW v1 used a `(req, res, ctx) => res(ctx.json(...))` signature that no longer exists. A resolver may also decline to mock: returning the value from `passthrough()` tells MSW to let the request go to the real network, and returning `undefined` lets the next matching handler try. ## What the application sees The important consequence is negative: nothing. The component is not handed a fake client, `fetch` is not swapped for a spy, and no import is rewired. The application builds a real `Request` and hands it to the platform; MSW intercepts it below that point and produces a real `Response` object, which then flows back through the app's normal parsing, error handling and state updates. Response headers, status codes and body parsing all behave the way they would in production, so the code path under test is the shipped one. This also means the handler is described in the vocabulary of the API, not of your module graph. If the team renames its HTTP wrapper, moves from `fetch` to another client, or reorganises where the call is made, the handlers keep working, because the only contract they encode is method + URL + payload. ## Where handlers live Because a handler is just a value with no environment coupling, the convention is one `handlers.js` module exporting an array, imported by whichever setup the current environment needs. A test file adds or replaces individual handlers for its own scenario rather than rebuilding the list. Keep the shared array describing the *happy path* of each endpoint your app calls, and let each test express only the deviation it cares about. ## Common mistakes Forgetting the method (using `http.get` for an endpoint the app POSTs to), writing a relative path when the app calls an absolute cross-origin URL, returning a fixture object instead of a `Response`, and ordering a catch-all pattern ahead of the specific one it shadows. All four present identically — the request appears unmocked — so check the predicate before you suspect the tooling.

  • How does the resolver get at the request body or the query string a component sent?
    The resolver receives the intercepted request as a standard `Request`. For a POST body, `await request.json()` (the resolver can be async). For the query string, `new URL(request.url).searchParams`. Named path segments arrive separately as `params`, so `http.get('/api/users/:id')` gives you `params.id` as a string. Asserting on those values is how you check the component sent what it should.
  • Two handlers in the array could both match one request. Which one answers it?
    MSW walks the handlers in order and the first matching predicate wins; the rest never run. So a broad pattern such as `http.get('/api/*')` placed before `http.get('/api/users')` will swallow the specific case. Order specific before general. A resolver can also return `undefined` to explicitly decline and let the next matching handler try.
  • What happens if a resolver returns a plain object like { id: 1 } instead of a Response?
    It is not treated as a mocked response. MSW v2 resolvers must return a `Response` — that is what `HttpResponse.json()` produces. Returning a bare object is the classic leftover from MSW v1's `res(ctx.json(...))` signature, which v2 removed. The symptom is a request that looks unhandled even though the handler clearly matched.

saying these in an interview costs you the question

  • Says MSW replaces the app's fetch with a stub function
  • Thinks the resolver returns a plain object, not a Response
  • Believes the component must be given a mock HTTP client
  • Assumes one handler covers every method on that path
  • Forgets path params arrive as strings, not numbers

context