A tool for intercepting network requests at the browser level. Allows frontend development independent of backend readiness using typed mocks.
Benefits: Allows testing error scenarios, network delays, and various data states without a real backend.
src/entities/[entity-name]/handlers/. Files named [entity-name].handlers.ts. Contain logic for intercepting entity-specific requests.src/entities/[entity-name]/mock/. Files named [entity-name].mock.ts. Contain static or dynamic data simulating a server response.src/shared/api/msw/. All handlers are registered in handlers.ts.backend DTO shape. Handlers must not return frontend models if RTK converters run in transformResponse.1import type { TEntityBackend } from "../types";
2
3/** Seed in backend DTO shape — never frontend models */
4export const ENTITY_MOCK: TEntityBackend[] = [
5 {
6 id: "1",
7 full_name: "John Doe",
8 created_at: "2024-03-22T10:00:00Z",
9 status_code: "active"
10 },
11 {
12 id: "2",
13 full_name: "Jane Smith",
14 created_at: "2024-03-21T10:00:00Z",
15 status_code: "pending"
16 }
17];reset for mutations and Vitest isolation. Skip for read-only static lists.1import type { TEntityBackend, TEntitiesBackend } from "../types";
2import { ENTITY_MOCK } from "./entity.mock";
3
4const createStore = () =>
5 new Map(ENTITY_MOCK.map((item) => [item.id, structuredClone(item)]));
6
7let store = createStore();
8
9/** Call from Vitest beforeEach for isolation — optional for read-only mocks */
10export const resetEntityStore = () => {
11 store = createStore();
12};
13
14export const listEntitiesBackend = (): TEntitiesBackend => ({
15 items: [...store.values()],
16 total_count: store.size
17});VITE_BASE_URL, delay (0 in tests), JSON body parse, and validation error helpers.1import { HttpResponse, delay, http } from "msw";
2import { ENV } from "@/shared/config";
3
4const BASE_URL = (ENV.VITE_BASE_URL || "").replace(/\/$/, "");
5const MOCK_RESPONSE_DELAY_MS = import.meta.env.MODE === "test" ? 0 : 500;
6
7export const createMockHandler = (endpoint, resolver) =>
8 http[endpoint.method.toLowerCase()](
9 `${BASE_URL}${endpoint.url}`,
10 async ({ request, params }) => {
11 await delay(MOCK_RESPONSE_DELAY_MS);
12 let body;
13 try {
14 body = await request.clone().json();
15 } catch {
16 /* no body */
17 }
18 return resolver({
19 body,
20 query: new URL(request.url).searchParams,
21 params,
22 request
23 });
24 }
25 );
26
27export const mockValidationError = (loc, msg) =>
28 HttpResponse.json(
29 { detail: [{ loc, msg, type: "value_error" }] },
30 { status: 422 }
31 );createMockHandler and return backend envelopes / error contracts matching the real API.1import { HttpResponse } from "msw";
2import {
3 createMockHandler,
4 mockValidationError
5} from "@/shared/api/msw/utils";
6import { listEntitiesBackend } from "../mock";
7
8/**
9 * Return backend DTO. RTK transformResponse + converters map to
10 * IPaginationResponse / domain models — do not return { data, total } here.
11 */
12export const entityHandlers = [
13 createMockHandler(
14 { url: "/entities", method: "GET", _types: { body: undefined } },
15 ({ query }) => {
16 const page = Number(query.get("page") ?? 1);
17 if (!Number.isInteger(page) || page < 1) {
18 return mockValidationError(["query", "page"], "Invalid page");
19 }
20 return HttpResponse.json(listEntitiesBackend());
21 }
22 )
23];shared/api/msw/browser.ts to intercept browser requests.1import { setupWorker } from "msw/browser";
2import { handlers } from "./handlers";
3
4export const worker = setupWorker(...handlers);setupServer in shared/api/msw/server.ts for unit/integration tests.1import { setupServer } from "msw/node";
2import { handlers } from "./handlers";
3
4/** Vitest / Node tests */
5export const server = setupServer(...handlers);shared/api/msw/handlers.ts.1import { entityOneHandlers } from "@/entities/entity-one/handlers";
2import { entityTwoHandlers } from "@/entities/entity-two/handlers";
3
4export const handlers = [
5 ...entityOneHandlers,
6 ...entityTwoHandlers
7];app/init/msw.ts that checks environment flags.1import { ENV } from "@/shared/config";
2
3export const initMsw = async () => {
4 if (import.meta.env.DEV && ENV.VITE_ENABLE_MSW === "true") {
5 const { worker } = await import("@/shared/api/msw/browser");
6 return worker.start({
7 onUnhandledRequest: "bypass"
8 });
9 }
10 return Promise.resolve();
11};src/main.tsx.1import { StrictMode } from "react";
2import { createRoot } from "react-dom/client";
3import { initMsw } from "./app/init/msw";
4import App from "./app";
5
6const container = document.getElementById("root");
7
8if (container) {
9 const root = createRoot(container);
10
11 initMsw().then(() => {
12 root.render(
13 <StrictMode>
14 <App />
15 </StrictMode>
16 );
17 });
18}.env file (VITE_ENABLE_MSW, VITE_BASE_URL).1VITE_ENABLE_MSW=true