Platform
    • RTK Query
    • Mock Server (MSW)
    • Data Converters
    • OpenAPI Codegen
Projects
  • API Connections & Tools
  1. Documentation
  2. Mock Server (MSW)

Mock Server (MSW)

Инструмент для перехвата сетевых запросов на уровне браузера. Позволяет разрабатывать фронтенд независимо от готовности бэкенда, используя типизированные моки.

Мокирование API (Документация MSW)

Преимущества: Позволяет тестировать сценарии ошибок, задержки сети и различные состояния данных без реального бэкенда.

Структура Mock-слоя

Правила именования и расположения

  • Хендлеры (Handlers):
    Располагаются в src/entities/[entity-name]/handlers/. Файлы называются [entity-name].handlers.ts. Содержат логику перехвата специфичных для сущности запросов.
  • Моки (Mocks):
    Располагаются в src/entities/[entity-name]/mock/. Файлы называются [entity-name].mock.ts. Содержат статические или динамические данные, имитирующие ответ сервера.
  • Конфигурация:
    Общие настройки MSW находятся в src/shared/api/msw/. Регистрация всех хендлеров происходит в handlers.ts.

011. Создание моков (backend DTO)

Seed в форме backend DTO. Handlers не должны отдавать frontend-модели, если converters работают в transformResponse.
entities/[entity-name]/mock/[entity-name].mock.ts
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];

01b1b. Опциональный Mock Store

Изменяемый store с reset для мутаций и изоляции Vitest. Для read-only списков не обязателен.
entities/[entity-name]/mock/[entity-name].store.ts
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});

022. createMockHandler

Общая фабрика на VITE_BASE_URL, delay (0 в тестах), разбор JSON body и хелперы ошибок валидации.
shared/api/msw/utils.ts
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 );

023. Описание хендлеров

Используем createMockHandler и возвращаем backend envelope / контракты ошибок реального API.
entities/[entity-name]/handlers/[entity-name].handlers.ts
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];

034. Browser Worker

Создаем worker в shared/api/msw/browser.ts для перехвата запросов в браузере.
shared/api/msw/browser.ts
1import { setupWorker } from "msw/browser"; 2import { handlers } from "./handlers"; 3 4export const worker = setupWorker(...handlers);

03b4b. Node Server (Vitest)

Создаем setupServer в shared/api/msw/server.ts для unit/integration тестов.
shared/api/msw/server.ts
1import { setupServer } from "msw/node"; 2import { handlers } from "./handlers"; 3 4/** Vitest / Node tests */ 5export const server = setupServer(...handlers);

045. Реестр хендлеров

Собираем хендлеры всех сущностей в единый массив в shared/api/msw/handlers.ts.
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];

056. Инициализация в App

Асинхронный запуск в app/init/msw.ts с проверкой флагов окружения.
app/init/msw.ts
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};

067. Точка входа (Main)

Дожидаемся запуска MSW перед рендерингом в src/main.tsx.
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}

078. Переменные окружения

Управление через .env (VITE_ENABLE_MSW, VITE_BASE_URL).
.env
1VITE_ENABLE_MSW=true