Skip to Content
GuidesMocking with MSW

Mocking with MSW

@routar/msw generates typed MSW v2 handlers directly from a router definition — no manual URL strings, no handler registration by hand.

Installation

npm install @routar/msw msw

Basic usage

import { createMswHandlers } from '@routar/msw' import { HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { TodoRouter } from '@/remote/services/todo' const handlers = createMswHandlers(TodoRouter, 'https://api.example.com', { getList: () => HttpResponse.json([{ id: 1, title: 'buy milk', completed: false }]), getDetail: ({ params }) => HttpResponse.json({ id: params.id, title: 'buy milk', completed: false }), }) const server = setupServer(...handlers)

The resolver map mirrors the router’s shape. All keys are optional — only registered endpoints get handlers; the rest pass through to the real server.

Typed resolver context

Each resolver receives a typed context derived from the endpoint’s request schema:

createMswHandlers(TodoRouter, BASE_URL, { // params.id is number when path schema uses z.coerce.number() getDetail: ({ params, query, body, request, cookies }) => HttpResponse.json({ id: params.id, title: 'Typed!' }), })
FieldTypeSource
paramsInferred from path schemaURL path params
queryInferred from query schemaURL query string
bodyInferred from body schemaRequest body (JSON)
requestRequestRaw MSW request object
cookiesRecord<string, string>Request cookies

Nested routers

Nest the resolver map to match the router’s shape:

const handlers = createMswHandlers(UserRouter, BASE_URL, { getList: () => HttpResponse.json([]), todos: { getList: () => HttpResponse.json([]), getDetail: ({ params }) => HttpResponse.json({ id: params.id, title: 'x' }), }, })

Path param coercion

MSW extracts path params as strings. Use z.coerce.number() instead of z.number() for numeric IDs so the schema coerces "42"42 before the resolver receives it:

// In the router definition: getDetail: endpoint({ path: '/:id', request: z.object({ path: z.object({ id: z.coerce.number() }) }), // ... })

The same applies to numeric query params — use z.coerce.number() for _limit, _page, etc., since URL query values are always strings.

Stateful mock store

For CRUD flows, share an in-memory store across handlers:

// mocks/stores/todo-store.ts let nextId = 11 const store = [{ id: 1, title: 'Buy milk', completed: false }] export const getAllTodos = () => store.slice() export const createTodo = (data) => { const t = { id: nextId++, ...data }; store.push(t); return t }
// mocks/handlers.ts import { getAllTodos, createTodo } from './stores/todo-store' const handlers = createMswHandlers(TodoRouter, BASE_URL, { getList: ({ query }) => HttpResponse.json(getAllTodos()), create: ({ body }) => HttpResponse.json(createTodo(body), { status: 201 }), })

Browser setup

// mocks/browser.ts import { setupWorker } from 'msw/browser' import { allHandlers } from './handlers' export const worker = setupWorker(...allHandlers)
// Activate in development (e.g. providers.tsx) if (process.env.NODE_ENV === 'development') { const { worker } = await import('@/remote/mocks/browser') await worker.start({ onUnhandledRequest: 'bypass' }) }

Run npx msw init public/ once to copy the service worker file into your public/ directory.

Last updated on