โ† All guides

Mock API for Storybook โ€” every data state as a story, and each story is just a URL

Honest split first: msw-storybook-addon is the community standard and it's in good shape โ€” v3.0.0 shipped July 2026 with support for Storybook โ‰ฅ9 and MSW 2. If your components hard-code a production URL and you want to intercept it transparently, offline, with zero latency, use it; this page won't pretend otherwise. But there's a second way to drive data-dependent stories that involves no handler code, no mockServiceWorker.js file, no loader config at all: point the component at a hosted mock endpoint and make each story a different query string. We built it and verified everything below on Storybook 10.5.10 (@storybook/react-vite), React 19.2, Vite 8.2, Node 22 โ€” the four stories render exactly as described, checked in a real browser against a static storybook build.

The whole idea in one file

A typical fetching component takes its endpoint from a prop (or an env var). Then the story matrix is nothing but URLs:

// ProductList.stories.jsx โ€” this exact file is what we verified
import { ProductList } from './ProductList';

const BASE = 'https://mockbird.mockbird.workers.dev/m/demo';

export default { title: 'ProductList', component: ProductList };

export const Default = {
  args: { url: `${BASE}/products?limit=5` },
};

export const Loading = {                    // 3 real seconds of skeleton
  args: { url: `${BASE}/products?limit=5&mock_delay=3000` },
};

export const ErrorState = {                 // real HTTP 500 + JSON error body
  args: { url: `${BASE}/products?mock_status=500` },
};

export const Empty = {                      // filter that matches nothing โ†’ []
  args: { url: `${BASE}/products?category=none-such` },
};

That's the entire setup. No .storybook/preview changes, no initialize(), no worker file in staticDirs, no handlers to keep in sync with your API. The /m/demo project above is our shared public playground โ€” those four stories work in your Storybook right now, unchanged, without signing up for anything.

What each param did when we ran it:

StoryURL trickVerified behavior
Default?limit=55 seeded products, stable ids
Loading?mock_delay=3000response held ~3s (we measured 3.28s wall-clock) โ€” the skeleton state is real, watchable, and demo-able to a designer
ErrorState?mock_status=500genuine HTTP 500 with a JSON body โ€” exercises your actual !r.ok branch
Empty?category=none-such[] โ€” the empty-state render, no fixture file

One thing that does not work: ?limit=0 is treated as unset and returns the default page, not an empty array. Use a filter that matches nothing (as above), or โ€” cleaner โ€” an empty snapshot on your own project, next section.

Whole scenarios per story: snapshot pinning

Filters get you an empty list; snapshots get you an entire alternate dataset per story โ€” "empty everything", "edge-case names", "bug #412 repro" โ€” all served from one project, read-only, without touching live data. Create your own project (one curl, no signup) and freeze the states once:

# 1. one seeded e-commerce backend
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H 'Content-Type: application/json' -d '{"name":"storybook","preset":"ecommerce"}'
# โ†’ { "id": "<project>", "adminKey": "<key>", ... }

# 2. freeze the seeded data as "baseline"
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots \
  -H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"name":"baseline"}'

# 3. wipe each resource (zero-count reseed), freeze as "empty", restore baseline
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/resources/products \
  -H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"seed":0}'
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots \
  -H 'x-admin-key: <key>' -H 'Content-Type: application/json' -d '{"name":"empty"}'
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects/<project>/snapshots/baseline/restore \
  -H 'x-admin-key: <key>'

Now a story pins its scenario with one more query param โ€” we ran this flow end-to-end while writing the page (live endpoint kept serving the restored baseline; the pinned URL returned []):

export const Empty = {
  args: { url: `${BASE}/products?mock_snapshot=empty` },
};

Snapshot reads are read-only and don't disturb the live dataset, so a colleague clicking through your deployed Storybook can't break the fixtures. Full recipe (including per-scenario snapshots for tests) in the deterministic test data guide.

More states, still zero handler code

You want a story forURL
Slow-network skeleton?mock_delay=3000 (up to 5000ms)
Any HTTP error?mock_status=500 โ€” or 401, 403, 429โ€ฆ
Error then recovery (retry UI)?mock_seq=500,200&mock_seq_key=story-retry
Pagination controls?_page=2&_limit=5 (+ X-Total-Count header)
Sparse fields / long stringsedit records in the dashboard, snapshot it
Flaky network demo?mock_chaos=0.3 โ€” 30% of requests fail randomly

Honest comparison: msw-storybook-addon vs a hosted mock URL

msw-storybook-addonMockbird
Setupinstall msw + addon, generate mockServiceWorker.js into your static dir, register the loader in preview, write handlersnone โ€” a story arg is a URL
Mocks your real production URL transparentlyyes โ€” component code untouchedno โ€” component must take a base URL (prop or env), which is good practice anyway
Works offline / zero latencyyesno โ€” real network call
Per-story response logicanything you can write in JS handlersquery params + custom routes for fixed payloads
Fixtures live inhandler code you maintain and keep honesta hosted dataset โ€” seeded, imported (OpenAPI, db.json, CSV, HAR), or hand-edited; snapshots freeze it
Stateful (a play function can POST and re-read)hand-rolled handler statedefault โ€” writes persist
Same data outside the Storybook tabno โ€” the mock exists only where the worker runsyes โ€” curl, tests, Playwright, a teammate's machine, the deployed preview all hit the same URL
Request capnone10,000/project/day (demo: 50k shared)

They also compose: keep the addon for stories that must intercept a hard-coded production URL, and point everything else at a Mockbird base URL via VITE_API_URL โ€” the pattern most component libraries already use.

Try it in 10 seconds

curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=5'
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=500'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?category=none-such'

All against the shared demo project (resets daily; CORS is open, so browser fetch() from any origin โ€” including your local Storybook โ€” just works). Or create your own with one click: open the dashboard with an e-commerce preset.

Written by the Mockbird maker โ€” bias disclosed. Where msw-storybook-addon genuinely wins: offline work, zero latency, arbitrary JS in handlers, and transparent interception of the exact URL your production code calls. It's actively maintained and we'd use it for those cases. When you'd rather have zero mock code in the repo and states you can hand to a designer as links, that's us.

Full API reference in the docs. More guides: mock API for React ยท MSW alternative ยท testing loading & error states ยท deterministic test data ยท mock APIs in Vitest ยท Playwright. Create your API โ†’