typescript 116 lines · 4 tabs

MSW for frontend API mocking in tests

Shared by codesnips Jan 2026
4 tabs
import { http, HttpResponse } from 'msw';

export interface Product {
  id: number;
  name: string;
}

export const PRODUCTS: Product[] = Array.from({ length: 45 }, (_, i) => ({
  id: i + 1,
  name: `Product ${i + 1}`,
}));

export const handlers = [
  http.get('/api/products', ({ request }) => {
    const url = new URL(request.url);
    const page = Number(url.searchParams.get('page') ?? '1');
    const limit = Number(url.searchParams.get('limit') ?? '20');

    const start = (page - 1) * limit;
    const items = PRODUCTS.slice(start, start + limit);
    const hasMore = start + limit < PRODUCTS.length;

    return HttpResponse.json({ items, page, hasMore });
  }),
];

export const errorHandler = http.get('/api/products', () => {
  return HttpResponse.json({ message: 'boom' }, { status: 500 });
});
4 files · typescript Explain with highlit

This snippet shows how Mock Service Worker (MSW) intercepts HTTP at the network layer so that a React data hook can be tested against realistic paginated responses instead of stubbed fetch functions. The advantage of MSW over jest.mock('fetch') is that the code under test issues real requests through the same client it uses in production; nothing about the fetching logic is faked, only the server on the other end.

The handlers.ts tab defines a single GET /api/products handler with http.get. It reads page and limit from the query string, slices a fixed in-memory PRODUCTS array, and returns HttpResponse.json with a hasMore flag. Modeling the boundary conditions here — the last page, an empty page, an oversized page — is what makes the handler valuable, because those are exactly the branches the hook must get right. The errorHandler demonstrates runtime overrides: tests can call server.use(errorHandler) to force a 500 for a single case without touching the default happy-path handler.

The server setup tab wires MSW into the test lifecycle. setupServer starts a Node-side interceptor; beforeAll boots it, afterEach calls server.resetHandlers() so per-test overrides don't leak, and afterAll tears it down. onUnhandledRequest: 'error' is deliberate — it turns any un-mocked request into a loud failure rather than a silent real network call.

The useProducts hook tab is the subject under test. It uses React Query's useInfiniteQuery, deriving the next page from getNextPageParam by inspecting lastPage.hasMore. Because the hook only knows about the HTTP contract, it needs no test-specific seams.

The useProducts.test.tsx tab exercises the real behavior: it renders the hook inside a QueryClientProvider with retry: false so the error path fails fast, waits for the first page with waitFor, then calls fetchNextPage and asserts the accumulated pages. The final test installs errorHandler and asserts result.current.isError, proving the failure branch. The main pitfall this avoids is asserting on loading flags too eagerly; waitFor polls until React Query settles, keeping the tests free of arbitrary timeouts.


Related snips

Share this code

Here's the card — post it anywhere.

MSW for frontend API mocking in tests — share card
Link copied