Skip to content
msw-devtools logo

@mugenlabs/msw-devtools

A TanStack DevTools plugin for managing MSW mocks.
Toggle, customize, and inspect your mock handlers in real time.

This project is a work in progress — the API has not been finalised yet, which is why we haven't reached 1.0.0. Expect breaking changes between minor versions.

Toggle Mocks
Switch Variants
Live Overrides
LIVE Tracking
Filter & Sort
Auto Refetch

Works with

TanStack QueryRTK QueryURQLApollo ClientSWRAxiosfetch

Why @mugenlabs/msw-devtools?

Tired of commenting out handlers, hard-coding error responses, and refreshing the page every time you need a different mock? @mugenlabs/msw-devtools lets you toggle, swap, and override any MSW mock on the fly — right from the browser, without touching your code.

Toggle Mocks

Enable or disable individual mock handlers with a single click. Disabled handlers pass requests straight through to the real network, so you can test real vs. mocked responses side by side — no code changes, no restarts.

RESTGET Charizard
→ 200 · mock · 21 ms
RESTGET Gengar

GET Charizard is mocked — instant, deterministic responses.

Switch Variants

Define multiple response variants for the same endpoint — success, empty list, validation error, 404 — and swap between them from a dropdown. Perfect for exploring every UI state without writing throwaway code.

GQLGetPancham
variantDefault▾
→ { name: "pancham", types: ["fighting"] }

GetPancham serves its default response…

Live Overrides

Edit response JSON, status codes, and headers directly in the panel. Need to test how your UI handles a 500? A missing field? Just change it and the response updates instantly — no handler code to touch.

status200401404500
{
"name": "charizard",
"weight": 905
}

The captured response, ready to edit.

LIVE Tracking

Every intercepted request is tracked in real time. Operations that are actively being called on the current page get a LIVE badge, so you can see at a glance which handlers are actually in use.

RESTGET CharizardLIVE
GQLGetPanchamLIVE
RESTGET GengarLIVE

The page loads quietly — nothing tracked yet.

Filter & Sort

As your mock list grows, use built-in filtering and sorting to quickly find handlers by name, HTTP method, or status. Filter by "live" to see only the operations active on the current page.

AllLiveRESTGraphQL
RESTGET CharizardLIVE
GQLGetPanchamLIVE
RESTGET GengarLIVE
GQLGetSnorlaxLIVE

36 handlers registered? The list view shows them all…

Auto Refetch

Register an adapter for your data-fetching library (TanStack Query, RTK Query, SWR, Apollo, URQL) and every mock change automatically invalidates the cache — your UI re-renders with fresh data without a page reload.

Panel
GetPancham
Default▾
⇢
Your app
pancham
fighting · 80 wt

Your app renders data from the mock, as usual.

Install the core library:

bash
npm install @mugenlabs/msw-devtools

This library renders inside TanStack DevTools, so you'll need that too:

bash
npm install @tanstack/react-devtools

The package is ESM-only and runs in the browser: MSW needs Service Worker support, so serve the app over HTTPS or from localhost. It is safe to import during server rendering: registering mocks there only records them, and the worker starts in the browser when the plugin mounts.

1Register your handlers

Pass your MSW handlers directly to registerRestMocks or registerGraphqlMocks. The operation name, method, and path are auto-derived from the handler. Each call returns type-safe OperationHandles you can destructure and pass to useMockRefetch instead of hard-coding name strings:

typescript
import { http, HttpResponse, graphql } from "msw";
import { registerRestMocks, registerGraphqlMocks } from "@mugenlabs/msw-devtools";

// REST — pass your HttpHandler directly; capture the returned handle
const [getUsers] = registerRestMocks(
  {
    handler: http.get("/api/users", () =>
      HttpResponse.json([{ id: 1, name: "Alice" }])
    ),
    group: "Users",
  },
);

// GraphQL — pass your GraphQLHandler directly
const [getUser] = registerGraphqlMocks(
  {
    handler: graphql.query("GetUser", () =>
      HttpResponse.json({ data: { user: { id: 1, name: "Alice" } } })
    ),
    group: "Users",
  },
);

// getUsers.operationName === "GET /api/users", getUser.operationName === "GetUser"
2Mount the DevTools plugin

Add the TanStack DevTools component to your app with the MSW plugin. The service worker starts automatically when the plugin mounts:

tsx
// App.tsx
import { TanStackDevtools } from "@tanstack/react-devtools";
import { createMswDevToolsPlugin } from "@mugenlabs/msw-devtools";
import "./mocks/setup"; // your registration calls

function App() {
  return (
    <>
      <YourApp />
      <TanStackDevtools plugins={[createMswDevToolsPlugin()]} />
    </>
  );
}
3Register adapters (optional)

Adapters connect your data-fetching library to the devtools. When you toggle a mock or switch variants, the adapter automatically refetches/revalidates so your UI updates immediately.

typescript
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createTanStackQueryAdapter } from "@mugenlabs/msw-devtools/adapters/tanstack-query";
import { createRtkQueryAdapter } from "@mugenlabs/msw-devtools/adapters/rtk-query";
import { createUrqlAdapter } from "@mugenlabs/msw-devtools/adapters/urql";
import { createApolloAdapter } from "@mugenlabs/msw-devtools/adapters/apollo";
import { createAxiosAdapter } from "@mugenlabs/msw-devtools/adapters/axios";

// Pick the adapters matching your stack:
registerAdapter(createTanStackQueryAdapter(queryClient));
registerAdapter(createRtkQueryAdapter(store, pokemonApi));
registerAdapter(createUrqlAdapter());
registerAdapter(createApolloAdapter(apolloClient));
registerAdapter(createAxiosAdapter());

Each adapter hooks into a specific library's cache invalidation mechanism. You only need to register the adapters for the libraries you actually use. Panel edits dispatch a CustomEvent with a MockChangeType — including status-override and headers-override so clients refetch the overridden response. Enable all / Disable all send one event whose operationName is ALL_OPERATIONS (*). Built-in cache adapters ignore the name and refetch everything; custom adapters that compare operationName with === miss those bulk events — use affectsOperation(event, name) instead.

TanStack Query

Invalidates all queries via queryClient.invalidateQueries() when mock config changes. Only queries with active subscriptions (i.e., components currently mounted and observing them) are refetched.

typescript
import { QueryClient } from "@tanstack/react-query";
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createTanStackQueryAdapter } from "@mugenlabs/msw-devtools/adapters/tanstack-query";

const queryClient = new QueryClient();
registerAdapter(createTanStackQueryAdapter(queryClient));
How it works

When you toggle a mock, switch variants, or edit status/headers/JSON in the devtools, a CustomEvent is dispatched on the window. Enable all / Disable all also fire once with operationName *. The TanStack Query adapter ignores the operation name and calls queryClient.invalidateQueries(), which marks every query in the cache as stale. React Query then automatically refetches only queries that have active subscriptions — that is, queries being observed by at least one mounted component. Inactive queries (those with no observers) are marked stale but not refetched until a component subscribes to them again. This keeps network usage efficient while ensuring every visible piece of data reflects the latest mock configuration.

URQL

Uses a custom exchange that re-executes active queries. Add mockRefetchExchange to your URQL client's exchange pipeline:

typescript
import { createClient, cacheExchange, fetchExchange } from "@urql/core";
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createUrqlAdapter, mockRefetchExchange } from "@mugenlabs/msw-devtools/adapters/urql";

registerAdapter(createUrqlAdapter());

const client = createClient({
  url: "/graphql",
  exchanges: [cacheExchange, mockRefetchExchange, fetchExchange],
});
How it works

URQL works differently from other adapters. The adapter itself is a no-op registration marker — the actual refetch logic lives in the mockRefetchExchange, a custom URQL exchange that you add to your client's exchange pipeline. The exchange tracks all active operations and listens for mock update events on the window. When a mock changes, it uses affectsOperation so both per-operation events and bulk * (Enable all / Disable all) match, then re-executes those queries with a "network-only" request policy, bypassing the cache and fetching fresh data from MSW.

Apollo Client

Calls apolloClient.refetchQueries() to re-run all active queries.

typescript
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createApolloAdapter } from "@mugenlabs/msw-devtools/adapters/apollo";

const apolloClient = new ApolloClient({ uri: "/graphql", cache: new InMemoryCache() });
registerAdapter(createApolloAdapter(apolloClient));
How it works

The Apollo adapter listens for mock update events and calls apolloClient.refetchQueries({ include: "active" }). This tells Apollo to re-run every query that currently has active observers (i.e., any component rendering data from a query). The queries bypass the cache and hit MSW again, picking up whatever mock variant is now active.

RTK Query

Resets the API state via api.util.resetApiState() when mock config changes. Only endpoints with active subscriptions (i.e., components currently mounted and using the generated hooks) are refetched.

typescript
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createRtkQueryAdapter } from "@mugenlabs/msw-devtools/adapters/rtk-query";

const pokemonApi = createApi({
  baseQuery: fetchBaseQuery(),
  endpoints: (builder) => ({
    getPokemon: builder.query({ query: (id) => `/api/pokemon/${id}` }),
  }),
});

const store = configureStore({
  reducer: { [pokemonApi.reducerPath]: pokemonApi.reducer },
  middleware: (gDM) => gDM().concat(pokemonApi.middleware),
});

// Pass the store and the RTK Query API instance
registerAdapter(createRtkQueryAdapter(store, pokemonApi));
How it works

The RTK Query adapter listens for mock update events and dispatches api.util.resetApiState(), which clears all cached data and resets every endpoint to its uninitialized state. RTK Query then automatically refetches only endpoints that have active subscriptions — that is, endpoints being used by at least one mounted component via the generated hooks (e.g., useGetPokemonQuery). Endpoints without active subscriptions are simply cleared and will fetch fresh data when a component subscribes to them again. This ensures every visible piece of data reflects the latest mock configuration without triggering unnecessary network requests.

SWR

Calls the global mutate() to revalidate all SWR keys.

tsx
import { useEffect } from "react";
import { useSWRConfig } from "swr";
import { registerAdapter } from "@mugenlabs/msw-devtools";
import { createSwrAdapter } from "@mugenlabs/msw-devtools/adapters/swr";

function SetupAdapter() {
  const { mutate } = useSWRConfig();

  useEffect(() => {
    const unregister = registerAdapter(createSwrAdapter(mutate));
    return unregister;
  }, [mutate]);

  return null;
}
How it works

The SWR adapter calls the global mutate() function with a matcher that returns true for all cache keys and no data argument. This tells SWR to revalidate every cached entry while keeping the current data on screen, triggering fresh network requests that hit MSW with the updated mock configuration. Because SWR's mutate is scoped to the provider context, the adapter must be registered inside a React component that has access to useSWRConfig().

Axios & plain fetch

Consider using a server-state library instead. Libraries like TanStack Query, RTK Query, SWR, or Apollo provide built-in caching, deduplication, and automatic refetching — making the integration with @mugenlabs/msw-devtools seamless and zero-config. The Axios/fetch adapter requires manual useMockRefetch wiring in every component, which adds boilerplate and is harder to maintain.

Axios and fetch() have no query cache. Register the Axios adapter as a marker, then use the useMockRefetch hook in your components to re-run requests when mock config changes:

tsx
import { registerAdapter, registerRestMocks, useMockRefetch } from "@mugenlabs/msw-devtools";
import { createAxiosAdapter } from "@mugenlabs/msw-devtools/adapters/axios";

registerAdapter(createAxiosAdapter());

// Registration returns type-safe operation handles
const [users] = registerRestMocks({ handler: usersHandler });

// In your component:
function UserCard() {
  const { data, refetch } = useMyFetch("/api/users/1");
  useMockRefetch(users, refetch); // typo-proof; raw strings still work too

  return <div>{data?.name}</div>;
}
How it works

Unlike the other libraries, Axios and fetch() don't have a built-in query cache or automatic refetch mechanism. The adapter itself is a no-op marker that registers the library with the devtools. To get live updates, you use the useMockRefetch hook in your components. This hook listens for mock update events matching a specific operation name (including bulk * Enable all / Disable all via affectsOperation) and calls your refetch callback when the mock changes, giving you per-operation control over what gets refreshed.

Most projects already have MSW handlers for unit tests, E2E, or Storybook. You can pass them directly to registerRestMocks or registerGraphqlMocks — no conversion needed.

Register handlers directly

Pass your existing MSW handlers directly. The devtools auto-derives the operation name, method, and path from each handler:

typescript
import { registerRestMocks, registerGraphqlMocks } from "@mugenlabs/msw-devtools";

// Your existing handlers — the same ones you use in tests
import { getUserHandler, getProductsHandler } from "./mocks/handlers";
import { getUserProfileQuery, updateSettingsMutation } from "./mocks/graphql-handlers";

// Register REST handlers
registerRestMocks(
  { handler: getUserHandler, group: "Users" },
  { handler: getProductsHandler, group: "Products" },
);

// Register GraphQL handlers
registerGraphqlMocks(
  { handler: getUserProfileQuery, group: "Users" },
  { handler: updateSettingsMutation, group: "Settings" },
);

Multiple variants for the same endpoint

If you have multiple success scenarios (e.g. full list vs empty list), use the variants array:

typescript
import { registerRestMocks } from "@mugenlabs/msw-devtools";
import { http, HttpResponse } from "msw";

registerRestMocks({
  variants: [
    http.get("/api/users", () => HttpResponse.json([{ id: 1, name: "Alice" }])),
    { handler: http.get("/api/users", () => HttpResponse.json([])), label: "Empty list" },
  ],
  group: "Users",
});

Tip: Error scenarios (401, 404, 429, 500, Network Error) are handled by the Error Override UI in the devtools panel — no need to create separate error variants.

The essentials you need to get started, plus advanced exports for custom integrations.

Core

The functions you'll use in every project.

registerRestMocks

OperationHandlesFor<Defs>
registerRestMocks<const Defs extends readonly RestMockDef[]>(...defs: Defs): OperationHandlesFor<Defs>

Register one or more REST mocks from MSW HttpHandlers. Operation metadata (method, path, operationName) is auto-derived from the handler. Returns type-safe operation handles — an array (destructurable in registration order) that is also indexable by operationName — to pass to useMockRefetch instead of raw strings.

ParameterTypeDefaultDescription
def.handlerHttpHandler—MSW HttpHandler (shorthand for single variant)
def.variantsHandlerVariantInput<HttpHandler>[]—Multiple handler variants for the same endpoint
def.operationNamestring—Override the auto-derived operation name
def.groupstring—Optional grouping label

registerGraphqlMocks

OperationHandlesFor<Defs>
registerGraphqlMocks<const Defs extends readonly GraphqlMockDef[]>(...defs: Defs): OperationHandlesFor<Defs>

Register one or more GraphQL mocks from MSW GraphQLHandlers. Operation metadata (operationName, operationType) is auto-derived from the handler. Returns type-safe operation handles — an array (destructurable in registration order) that is also indexable by operationName — to pass to useMockRefetch instead of raw strings.

ParameterTypeDefaultDescription
def.handlerGraphQLHandler—MSW GraphQLHandler (shorthand for single variant)
def.variantsHandlerVariantInput<GraphQLHandler>[]—Multiple handler variants for the same operation
def.operationNamestring—Override the auto-derived operation name
def.operationType"query" | "mutation" | "operation"—Override the auto-derived operation type
def.groupstring—Optional grouping label

createMswDevToolsPlugin

TanStackDevtoolsReactPlugin
createMswDevToolsPlugin(options?: MswDevToolsPluginOptions): TanStackDevtoolsReactPlugin

Creates a TanStack DevTools plugin that auto-starts the MSW service worker on mount. No manual worker setup required.

ParameterTypeDefaultDescription
options.defaultOpenbooleantrueWhether the devtools panel starts open
options.namestring'MSW Mocks'Plugin display name

registerAdapter

() => void
registerAdapter(adapter: MswDevToolAdapter): () => void

Register a data-fetching adapter. When mock config changes, the adapter automatically refetches queries. Returns an unregister function for cleanup. Change kinds include toggle, variant, json-override, status-override, headers-override, and bulk enable-all / disable-all.

React Hooks

React hooks for integrating with the mock system.

useMockRefetch

void
useMockRefetch(operation: OperationHandle | string, refetch: () => void): void

Listens for mock update events matching the given operation and calls your refetch callback. Accepts either an OperationHandle (recommended — returned from registration, so typos are caught at build time) or a raw operation name string. Matches per-operation events and bulk Enable all / Disable all via affectsOperation. Use with Axios or plain fetch.

tsx
const [users] = registerRestMocks({ handler: usersHandler });

function UserCard() {
  const { data, refetch } = useMyFetch("/api/users/1");
  useMockRefetch(users, refetch); // or useMockRefetch("GET /api/users/1", refetch)
  return <div>{data?.name}</div>;
}

Advanced

For custom integrations. Most apps won't need these.

startWorker

Promise<SetupWorker>
startWorker(options?: WorkerOptions): Promise<SetupWorker>

Manually start the MSW service worker with custom options. The plugin auto-starts the worker on mount — only use this if you need a custom serviceWorkerUrl or other non-default options. If the worker is already running, the plugin skips auto-start.

ParameterTypeDefaultDescription
options.onUnhandledRequest'bypass' | 'warn' | 'error''bypass'How to handle requests with no matching handler
options.quietbooleantrueSuppress MSW console logging
options.serviceWorkerUrlstring'/mockServiceWorker.js'Custom path to the service worker script
typescript
// Example: app deployed under a sub-path (e.g. GitHub Pages)
import { startWorker } from "@mugenlabs/msw-devtools";

void startWorker({
  serviceWorkerUrl: "/my-app/mockServiceWorker.js",
});

stopWorker

Promise<void>
stopWorker(): Promise<void>

Stops the MSW service worker started by startWorker() and detaches everything the devtools attached to it (registry subscription, request tracker, SPA navigation patch). Registered mocks and persisted configuration are kept, so a later startWorker() resumes where it left off. Useful in tests and hot-reload setups.

ALL_OPERATIONS

"*"
const ALL_OPERATIONS: "*"

Sentinel operationName ("*") on bulk enable-all / disable-all events. Built-in cache adapters ignore the name and refetch everything; custom adapters that compare operationName with === miss Enable all / Disable all. Match with affectsOperation instead.

affectsOperation

boolean
affectsOperation(event: MockUpdateEvent, operationName: string): boolean

Returns true when a mock update event applies to the given operation — either a per-operation match or a bulk ALL_OPERATIONS ("*") event. useMockRefetch and the URQL mockRefetchExchange already use this.

MockChangeType

union
"toggle" | "variant" | "json-override" | "status-override" | "headers-override" | "enable-all" | "disable-all"

Change kind on each adapter event. Status code and header edits dispatch "status-override" and "headers-override" so clients refetch the overridden response. Enable all / Disable all dispatch a single event whose operationName is ALL_OPERATIONS.