Honest split first: fetch-mock earned its ~1.2 million weekly downloads. It has mocked fetch since before fetch was in Node, the modern fetchMock.mockGlobal().get(url, response) API is clean, and โ unlike some rivals' fake timeouts โ its { delay: 500 } option makes real time pass (we measured 509ms). For fast offline unit tests of fetch-based code it's still a fine choice. This page is not going to pretend otherwise.
We verified all of these the week of writing (Aug 2026, fetch-mock v12.6.0, Vitest 4, Node 22) โ every snippet on this page was actually run:
fetchMock.mock(matcher, response) โ the call in a decade of blog posts, StackOverflow answers and READMEs โ is simply gone: on v12, typeof fetchMock.mock is not a function. The replacement is mockGlobal() + .route()/.get()/.post(), plus separate @fetch-mock/jest and @fetch-mock/vitest wrappers.fetch-mock: No response or fallback rule to cover get to โฆ โ loud and clear, which is genuinely better than a mystery 404. But it means every endpoint your code touches needs a handler you write and maintain.ONLY-IN-TEST-PROCESS, then had a child Node process fetch the identical URL: the child got the real network response (20 records), not the stub. Anything outside your test process โ a dev server you spawn, a worker, a backend, curl โ never sees your mock.getOnce(url, 503) then get(url, 200) gives you a one-shot failure sequence, and callHistory records calls โ but if you want a POST to actually change what the next GET returns, you build that yourself in handler code.If the process-boundary bullet is your actual problem, no in-process library fixes it โ the mock needs to be somewhere every client can reach:
Mockbird is the other half: a hosted mock REST API on a real URL. Define resources (or pick a preset, or import an OpenAPI spec / db.json / CSV / Postman collection), get realistic seeded data and full stateful CRUD โ reachable from fetch, a browser, curl, a phone, another service, CI, or a teammate. No handler code, no version migrations, no signup.
# one curl, no signup โ a whole seeded e-commerce backend
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' -d '{"name":"shop","preset":"ecommerce"}'
# โ {"id":"abc123","adminKey":"KEY", ...} โ save both
curl https://mockbird.mockbird.workers.dev/m/abc123/products?limit=3
# โ 3 seeded products, CORS on, writes persist
Then in your app, nothing changes but the base URL:
const res = await fetch(`${process.env.API_URL}/products`);
// dev/test: API_URL=https://mockbird.mockbird.workers.dev/m/abc123
// prod: API_URL=https://api.yourapp.com
Prefer clicking? This link creates the same project in your browser โ no account. Have an OpenAPI spec? Paste it at /app#import and get a live seeded mock of your actual API shape.
| fetch-mock (v12) | Mockbird | Notes |
|---|---|---|
fetchMock.mockGlobal() | no code โ point the base URL at the mock | no process boundary: subprocesses, curl, browsers, teammates all see the same mock |
.get("/products", data) | a products resource | list/get/create/update/delete generated, plus filtering, sorting, pagination, relations |
| Hand-rolled response data | seeded realistic records | faker-style names/emails/prices/dates; or import your exact records from db.json/CSV |
.get(url, { status: 500 }) | ?mock_status=500 on any URL | no handler edit โ error/loading states guide |
.getOnce(url, 503) then .get(url, 200) | ?mock_seq=503,200 | deterministic per-caller sequences (add &mock_seq_key= to isolate parallel tests) โ retry logic sees real 503s then recovery |
{ delay: 500 } | ?mock_delay=500 | both make real time pass (we measured; credit to fetch-mock here) โ plus ?mock_jitter=100-1500 and ?mock_chaos=0.3 random failures |
callHistory.called(url) / .calls() | request inspector | last 50 requests: method, path, query, headers, body โ assert what was actually sent |
removeRoutes() / clearHistory() between tests | snapshot pinning | save named data states; each parallel test pins one via X-Mockbird-Snapshot โ no reset races |
| Unmatched call throws | n/a โ real endpoints answer real queries | any query composes: filters, ?select=, sort, pagination |
unmockGlobal() | flip the env var back | your app code never knew the difference |
# seeded data with field projection
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=2&select=name,price'
# the states you'd write handlers for โ as query params
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=503'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=2000'
# getOnce-style failure sequence, no handler: 503 then 200 then 200
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_seq=503,200&mock_seq_key=me1'
# stateful: the write persists (the thing handler code can't do for free)
curl -X POST https://mockbird.mockbird.workers.dev/m/demo/products \
-H 'content-type: application/json' -d '{"name":"proof","price":9.99}'
# โ returns {"id":31,โฆ} โ and GET /m/demo/products/31 now works
All against the shared demo project (resets daily).
spyGlobal() and .spy() pass through to the real network โ we ran both against our live demo: stub one endpoint with .get(url, { status: 500 }).spy() and every other request flows through to the hosted mock's real seeded data. One process, in-process stubs and a real stateful backend behind them.?mock_delay=3000 makes a real AbortSignal.timeout(1000) genuinely fire, and ?mock_chaos=0.5 gives your retry/backoff real 5xx storms to absorb โ recipes in the Node.js guide.GET /m/<project>/msw.js returns an MSW v2 handlers module with your project's current data baked in โ shape data in the dashboard, then run offline unit tests from generated code. No lock-in in either direction.| fetch-mock | Mockbird | |
|---|---|---|
| What it is | free OSS library (fetch only) | free hosted service |
| Reachable from | the JS process you call mockGlobal() in | anything with HTTP: fetch, browser, curl, Postman, mobile, backend, CI, agents |
| Subprocesses / other clients | not intercepted (verified: child process hit the real network) | it's a real URL โ every client works by definition |
| Setup | write + maintain handlers (across a v9โv12 API break) | one curl or one click; no code |
| Mock data | you write it | seeded realistic data, or import your own |
| Stateful CRUD | DIY | default โ writes persist |
| Error/latency simulation | per-handler; {delay} is real time (credit) | ?mock_status / ?mock_delay / ?mock_seq / ?mock_chaos / ?mock_jitter on any URL |
| Unmatched request | throws (clear error, but every endpoint needs a handler) | n/a โ real endpoints answer real queries |
| Works offline | yes | no โ it's a real network call |
| Latency | zero (in-process) | real network latency |
| Request assertions | callHistory, precise | request inspector (last 50, with headers/body) |
| Version situation | v12.6.0 current; 37% of installs still pin v9 (Nov 2020) โ npm registry, as of Aug 2026 | hosted โ nothing to pin or migrate |
| Request cap | none | 10,000/project/day |
Written by the Mockbird maker โ bias disclosed. Where fetch-mock genuinely wins: in-process interception means zero latency, zero network flakiness, tests that run on a plane; { delay } makes real time pass (unlike some rivals' instant fake timeouts); unmatched-call errors are loud and actionable; callHistory assertions are more precise than any log-based check; response functions are arbitrary JavaScript; and it's actively maintained โ v12.6.0 shipped with fresh @fetch-mock/jest and @fetch-mock/vitest wrappers. For offline unit tests of fetch-based code, keep it (on v12, with the new API). When the thing you need is a URL โ for a subprocess, a browser, a prototype, a teammate, a mobile client, or CI hitting a deployed build โ that's us.
Full API reference in the docs. More guides: mock API for Node.js ยท nock alternative ยท Polly.JS alternative ยท MSW alternative ยท axios-mock-adapter alternative ยท mock APIs in Jest ยท mock APIs in Vitest ยท free mock API tools compared ยท deterministic test data ยท testing loading & error states. Create your API โ