As frontend developers, we often face a common bottleneck: the backend API isn’t ready, or it’s unstable.
To keep working, we usually resort to hardcoding data in our components or simulating network delays with setTimeout. While this works temporarily, it creates “technical debt.” When the real API is finally ready, we have to go back, delete the fake code, and wire up the real endpoints.
In our project, we solved this using MSW (Mock Service Worker). While MSW is standard for unit testing, we found it incredibly powerful when integrated directly into our development environment.
The Concept
The idea is simple: don’t fake the component logic; intercept the network request.
MSW works by using a Service Worker to intercept HTTP requests. If the request matches a defined handler, MSW returns a mocked response. To your application, it looks exactly like a real network call.
The best part? We use the exact same mock handlers for our Vitest unit tests and our local development server.
How We Implemented It
Here is a look at how we integrated MSW into our React application efficiently.
1. The Entry Point (main.tsx)
Performance is key. We do not want MSW code in our production bundle. We use a dynamic import to lazy-load the worker only when specific environment variables are set.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { IS_DEV_ENV, IS_MSW_ENABLED } from "@/constants";
import App from "./App";
const initApp = async () => {
// Only start the worker if we are in Dev and MSW is enabled
if (IS_DEV_ENV && IS_MSW_ENABLED) {
// Dynamic import ensures this code is tree-shaken out of production
const { worker } = await import("@/tests/browser");
await worker.start();
}
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
};
initApp();
2. Selective Interception (browser.ts)
In a real-world scenario, you rarely mock everything. You might want to mock a new feature while letting other requests hit the real API.
We created a “Middleware” handler that sits at the top of our handler list. It acts as a gatekeeper, deciding which requests should be mocked and which should go to the real internet.
import { MSW_ENDPOINTS_TO_INTERCEPT } from "@/constants";
import { http, passthrough, setupWorker } from "msw/browser";
import fallbackHandlers from "./server/handlers";
const handlers = [
// The Gatekeeper Handler
http.all("*", ({ request }) => {
// 1. Always allow external services (like Sentry) to pass through
if (request.url.includes("sentry.your-company.com")) {
return passthrough();
}
// 2. Check if the URL is in our "Intercept List"
const isTargeted = MSW_ENDPOINTS_TO_INTERCEPT.some((url) =>
request.url.includes(url)
);
// 3. If it's NOT targeted, let it pass through to the real server
if (!isTargeted) {
return passthrough();
}
// 4. If we are here, we want to mock it.
// We return nothing so MSW continues to the specific handlers below.
return;
}),
// Spread our actual feature handlers
...fallbackHandlers,
];
export const worker = setupWorker(...handlers);
3. Dynamic Responses
One major advantage of this approach is simulating edge cases. We created a controller helper that allows us to force specific states (like “Error 500” or “Empty List”) without changing code.
In our handlers:
http.get(`${API_URL}features`, () => {
// Check the global test state
const status = getEndpointStatus();
if (status === "error") {
return new HttpResponse(null, { status: 500 });
}
// Return happy path data...
});
The Workflow
We control this behavior using .env.local variables. This makes it very easy to switch contexts without touching the code.
When we start a new feature, we define the route we want to build:
VITE_MSW_ENABLED=true
VITE_MSW_ENDPOINTS_TO_INTERCEPT=/my-new-feature
Now, fetch('/my-new-feature') returns our mock data immediately. The rest of the app continues to fetch data from the real backend. When the backend team finishes their work, we just remove that endpoint from the env variable, and the app automatically switches to the real API.
Conclusion
By moving mocks out of components and into the network layer, we:
- Reuse code: The same mocks power our Vitest suite and our manual testing.
- Develop faster: We aren’t blocked by backend availability.
- Keep code clean: No temporary hardcoded data in React components.
If you haven’t tried MSW in the browser yet, give it a shot. It bridges the gap between testing and development beautifully.
