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 mswBasic 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!' }),
})| Field | Type | Source |
|---|---|---|
params | Inferred from path schema | URL path params |
query | Inferred from query schema | URL query string |
body | Inferred from body schema | Request body (JSON) |
request | Request | Raw MSW request object |
cookies | Record<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.