Without a stub, await ProductList() gets as far as a socket: TypeError: fetch failed / Error: getaddrinfo ENOTFOUND api.example.com. Replacing globalThis.fetch hides that, along with every mistake in the URL, method and status handling. Mock Service Worker 668,066 (mswjs/msw (https://github.com/mswjs/msw 18,227 ), MIT, npm 2,036 i -D msw) intercepts a layer lower: in Node it patches the HTTP client, so your code still builds a real Request and parses a real Response.
export const handlers = [
http.get('https://api.example.com/api/products', () =>
HttpResponse.json([{ sku: 'MUG-1', name: 'Enamel mug', priceCents: 1200 }]),
),
];mocks/server.ts is one line, export const server = setupServer(...handlers) from msw/node. The Vitest 102,989 setup file starts it once per worker with beforeAll(() => server.listen({ onUnhandledRequest: 'error' })), then calls server.resetHandlers() in afterEach and server.close() in afterAll.
That option earns its keep: a request you did not declare fails the test instead of escaping to the network, so a typo in a path surfaces immediately. resetHandlers undoes per-test overrides, which is how you reach otherwise untestable failure paths — server.use() a handler returning HttpResponse.json({}, { status: 500 }), then assert await expect(ProductList()).rejects.toThrow('Products API returned 500'). The handlers also drive a browser service worker, so a Playwright 26,280 suite can reuse them.