Community TypeScript Snippet
Why I Stopped Mocking `fetch` and Reached for MSW
The two-handler MSW setup I drop into every Vitest project. Pattern-matched URL routing, typed JSON responses, and per-test overrides without re-importing the server.
Why I Stopped Mocking `fetch` and Reached for MSW
The two-handler MSW setup I drop into every Vitest project. Pattern-matched URL routing, typed JSON responses, and per-test overrides without re-importing the server.
By @ezb1981
May 8, 2026
·
Updated May 18, 2026
1,029 views
26
4.4 (9)
This is the pattern I see in most codebases: globalThis.fetch = jest.fn(...) and a manual response table. It works for one test, then someone forgets to reset between tests and you get bleed-through that only shows up in CI when the test order shuffles. The deeper issue is that you are mocking the wrong layer: your code uses fetch, axios, ky, or a generated SDK, and each one has slightly different semantics, so the mock has to be re-implemented per library. Stage two shows the pivot.
MSW's handler-as-data shape is the part that makes it scale. You declare http.get(url, resolver) once per default-happy-path response and let every test inherit them, which removes most of the per-test boilerplate. The setupServer returned object exposes listen, resetHandlers, and use, which slot into Vitest or Jest's beforeAll / afterEach hooks. The fake we built here mirrors that contract closely; in real MSW, the difference is that handlers can also intercept Service Worker traffic in the browser, but the Node API surface is identical. Once you have this you stop writing jest.spyOn(globalThis, 'fetch') and your tests stop interfering with each other.
The ergonomic move is server.use(...) to push an extra handler in front of the defaults, run the test, then server.resetHandlers() in afterEach to restore. This is the pattern that lets you keep your default handler list as the documented happy path while one specific test exercises the 503 branch without polluting the next test. In practice I keep the try/finally shape inside the test so a thrown assertion still triggers the reset; relying on afterEach alone fails if beforeEach did not run because of an earlier error. The result is a test suite where you can grep server.use( and see every override at a glance.
