Sharing MSW Handlers Between Tests and Browser
The most powerful pattern in MSW is writing handlers once and reusing them in your test suite, Storybook, and even your development server. This eliminates handler duplication and ensures your mocked APIs behave identically everywhere, making it trivial to add a new story or test—the handlers are already defined and working correctly.
When I introduced this pattern at Beacon Tech, our team went from maintaining three separate sets of mocks (Jest, Storybook, E2E) to a single source of truth. Onboarding new developers became simpler because examples all used the same handlers, and changing an API response required updating only one place.
Organizing Handlers Into a Shared Library
Create a mocks/ directory at the root of your project with separate files for different API domains or features, then export all handlers from a central index.
// src/mocks/handlers/articles.js
import { http, HttpResponse } from "msw";
export const articleHandlers = [
http.get("/api/articles", () => {
return HttpResponse.json({
articles: [
{ id: "1", title: "React Patterns", author: "Alice" },
{ id: "2", title: "GraphQL Best Practices", author: "Bob" },
],
});
}),
http.get("/api/articles/:id", ({ params }) => {
return HttpResponse.json({
id: params.id,
title: "Sample Article",
body: "Article content...",
});
}),
];
// src/mocks/handlers/users.js
import { http, HttpResponse } from "msw";
export const userHandlers = [
http.get("/api/users", () => {
return HttpResponse.json({
users: [
{ id: "1", name: "Alice", email: "[email protected]" },
{ id: "2", name: "Bob", email: "[email protected]" },
],
});
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: "3", ...body, created: true },
{ status: 201 }
);
}),
];
// src/mocks/handlers/index.js
export { articleHandlers } from "./articles";
export { userHandlers } from "./users";
Organizing handlers by domain makes it easy to find and modify handlers. The central index exports them, keeping the imports simple in your test setup and Storybook configuration.
Using Shared Handlers in Jest Tests
Your test setup imports the shared handlers without duplication.
// src/setupTests.js
import { setupServer } from "msw/node";
import { articleHandlers, userHandlers } from "./mocks/handlers";
export const server = setupServer(...articleHandlers, ...userHandlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Tests inherit the shared handlers and can override specific ones per test.
// src/components/ArticleList.test.js
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { server } from "../setupTests";
function ArticleList() {
const [articles, setArticles] = React.useState([]);
React.useEffect(() => {
fetch("/api/articles")
.then((res) => res.json())
.then((data) => setArticles(data.articles));
}, []);
return (
<ul>
{articles.map((a) => (
<li key={a.id}>{a.title}</li>
))}
</ul>
);
}
test("renders articles from shared handlers", async () => {
render(<ArticleList />);
const title = await screen.findByText("React Patterns");
expect(title).toBeInTheDocument();
});
test("handles missing articles", async () => {
server.use(
http.get("/api/articles", () => {
return HttpResponse.json({ articles: [] });
})
);
render(<ArticleList />);
const message = await screen.findByText(/no articles/i);
expect(message).toBeInTheDocument();
});
The first test uses the shared handlers; the second overrides just the articles endpoint for that test. No duplication.
Using Shared Handlers in Storybook
Storybook stories can use the same handlers, eliminating the need to manually create story data or stub props.
// .storybook/preview.js
import { setupServer } from "msw/node";
import { articleHandlers, userHandlers } from "../src/mocks/handlers";
// Create a Storybook MSW server
const server = setupServer(...articleHandlers, ...userHandlers);
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterEach(() => server.resetHandlers());
export const decorators = [
(Story) => (
<div style={{ padding: "2rem" }}>
<Story />
</div>
),
];
// src/components/ArticleList.stories.js
import ArticleList from "./ArticleList";
import { http, HttpResponse } from "msw";
export default {
component: ArticleList,
title: "Components/ArticleList",
parameters: {
msw: {
handlers: [],
},
},
};
export const Default = {
render: () => <ArticleList />,
};
export const EmptyState = {
render: () => <ArticleList />,
parameters: {
msw: {
handlers: [
http.get("/api/articles", () => {
return HttpResponse.json({ articles: [] });
}),
],
},
},
};
export const Loading = {
render: () => <ArticleList />,
parameters: {
msw: {
handlers: [
http.get("/api/articles", async () => {
await delay(5000);
return HttpResponse.json({ articles: [] });
}),
],
},
},
};
Stories use parameters.msw.handlers to override the shared handlers for specific scenarios. This is much cleaner than hardcoding story data or mocking props.
Using Shared Handlers in Your Dev Server
During development, you might want to mock API calls if your backend is slow or unavailable. Add MSW to your dev server setup to provide the same mocked API experience as your tests.
// src/main.jsx (Vite entry point)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
// Enable API mocking in development
if (process.env.NODE_ENV === "development") {
const { articleHandlers, userHandlers } = await import("./mocks/handlers");
const { setupWorker } = await import("msw/browser");
const worker = setupWorker(...articleHandlers, ...userHandlers);
await worker.start({
onUnhandledRequest: "bypass", // Let real requests through
});
}
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
With this setup, running npm run dev automatically mocks your API, eliminating the need to start a backend or manage test data in your database.
Building a Feature-Specific Handler Set
Create handler sets for specific features or features states, making it easy to test different scenarios.
// src/mocks/handlers/scenarios.js
import { http, HttpResponse, delay } from "msw";
import { articleHandlers as defaultArticleHandlers } from "./articles";
// Scenario: Slow network
export const slowNetworkHandlers = [
http.get("/api/articles", async () => {
await delay(3000);
const [defaultHandler] = defaultArticleHandlers.filter(
(h) => h.hasOwnProperty("path") && h.path.includes("articles")
);
return defaultHandler?.(...arguments); // Delegate to default handler after delay
}),
];
// Scenario: Server errors
export const serverErrorHandlers = [
http.get("/api/articles", () => {
return HttpResponse.json(
{ error: "Server error" },
{ status: 500 }
);
}),
];
// src/components/ArticleList.test.js
import { server } from "../setupTests";
import { slowNetworkHandlers, serverErrorHandlers } from "../mocks/handlers/scenarios";
describe("ArticleList", () => {
test("shows loading state on slow network", async () => {
server.use(...slowNetworkHandlers);
// Test that loading spinner appears while data loads
});
test("shows error when server fails", async () => {
server.use(...serverErrorHandlers);
// Test that error message appears
});
});
Scenario-specific handler sets make it easy to test different failure modes and network conditions without writing custom handlers in every test.
Exporting and Reusing Fixture Data
Handlers often define data (articles, users, etc.). Extract this data into fixtures and reuse it in handlers, stories, and tests.
// src/mocks/fixtures.js
export const articleFixture = {
id: "1",
title: "React Patterns",
author: "Alice",
content: "Patterns and best practices for React...",
createdAt: "2026-05-15T10:00:00Z",
};
export const userFixture = {
id: "1",
name: "Alice",
email: "[email protected]",
role: "admin",
};
// src/mocks/handlers/articles.js
import { articleFixture } from "../fixtures";
export const articleHandlers = [
http.get("/api/articles/:id", ({ params }) => {
return HttpResponse.json({
...articleFixture,
id: params.id,
});
}),
];
// src/components/ArticleDetail.test.js
import { articleFixture } from "../../mocks/fixtures";
test("renders article with all fields", () => {
render(<ArticleDetail article={articleFixture} />);
expect(screen.getByText(articleFixture.title)).toBeInTheDocument();
});
Centralizing fixture data ensures consistency across tests, stories, and handlers.
Comparison: Mock Strategies
| Strategy | Where | Reusable | Effort | Realistic |
|---|---|---|---|---|
| Hardcoded story props | Storybook only | No | Low | No |
| Jest.mock() | Tests only | No | Low-Medium | No |
| Shared handlers | Tests, Storybook, Dev | Yes | Medium | Yes |
| Real backend | All | Yes | High | Yes |
Shared MSW handlers strike the best balance: highly reusable, realistic network behavior, and moderate setup effort.
Key Takeaways
- Organize handlers into a shared library by domain, then export from a central index.
- The same handlers work in Jest tests, Storybook, and your dev server.
- Override shared handlers per-test or per-story using
server.use()or Storybook'sparameters.msw.handlers. - Extract fixture data into a separate file to avoid duplication between handlers and tests.
- Scenario-specific handler sets (slow network, server errors) make it easy to test different conditions.
Frequently Asked Questions
Can I share handlers across multiple projects?
Yes, create a separate npm package or a monorepo shared package with your handlers and fixtures. Import them in each project's test setup and Storybook configuration.
What if I want different handlers in dev vs. test?
Use environment variables or create separate handler sets: const handlers = process.env.NODE_ENV === "test" ? testHandlers : devHandlers
How do I handle API versioning with shared handlers?
Create handler files per version: handlers/v1/, handlers/v2/. Import the correct version in your setup and Storybook configuration based on your API version.
Can I use shared handlers with E2E tests (Cypress, Playwright)?
E2E tests run in a real browser where you cannot import Node.js modules directly. Instead, start your dev server with MSW enabled (as shown above) and point your E2E tests to localhost:5173.