โ† All guides

Mock a REST API for Node.js โ€” fetch, axios and node:test against a real URL

Node has excellent libraries for faking HTTP inside one process: nock intercepts requests at the module level, axios-mock-adapter stubs one axios instance, undici ships a built-in MockAgent, and MSW patches fetch with service-worker semantics. They're the right tools for fast unit tests โ€” and this guide's comparison table says so plainly.

But an intercepted request can't help when you need a URL:

The boring fix is a hosted mock API. Every snippet below was run verbatim against the live demo project before publishing (Node 22) โ€” you can paste and run them too. Native fetch needs Node 18+.

1. Get an API (10 seconds, no signup)

curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H 'content-type: application/json' -d '{"name":"node-demo"}'
# โ†’ {"id":"abc123","adminKey":"...","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123", ...}

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/resources \
  -H 'content-type: application/json' -H 'x-admin-key: YOUR_KEY' \
  -d '{"name":"users","template":"users","seed":50}'

That's 50 realistic users (names, emails, avatars, cities) with full CRUD, filtering, sorting, search and pagination. The snippets below use the public demo project so they run with zero setup.

2. fetch quickstart โ€” real pagination headers

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

const r = await fetch(`${BASE}/products?page=1&limit=5&sortBy=price&order=desc`);
if (!r.ok) throw new Error(r.status);
const products = await r.json();
const total = Number(r.headers.get("x-total-count"));   // real header, not a fixture
console.log(`${total} products total; page of ${products.length}`);

Output when we ran it: 30 products total; page of 5. Exact-match filters (?category=tools), range operators (?price_gte=100), substring match (?name_like=river) and full-text ?q= all compose โ€” see the parameter reference.

3. Writes are real

const created = await (await fetch(`${BASE}/products`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ name: "Test Widget", price: 9.99 }),
})).json();                                              // 201 + the stored record

const back = await (await fetch(`${BASE}/products/${created.id}`)).json();
console.assert(back.name === "Test Widget");             // it persisted
await fetch(`${BASE}/products/${created.id}`, { method: "DELETE" });  // 404s afterwards

PUT/PATCH/DELETE behave like a real backend. (This is the part JSONPlaceholder, FakeStoreAPI and DummyJSON fake โ€” details with receipts in their respective guides.) Add ?mock_validate=1 to get real 422s with per-field errors when the body doesn't match the schema.

4. Sad paths: force any status, hit a real timeout

// any status code on demand
const err = await fetch(`${BASE}/products?mock_status=503`);
console.assert(err.status === 503);

// a genuinely slow response vs your client timeout
try {
  await fetch(`${BASE}/products?mock_delay=3000`, { signal: AbortSignal.timeout(1000) });
} catch (e) {
  console.log("timeout branch actually ran:", e.name);   // "TimeoutError"
}

That second one matters: an in-process interceptor answers instantly, so your AbortSignal.timeout() handling is never truly exercised. Here the server really holds the response for 3 seconds and the abort really fires.

5. Exercise real retry logic with injected chaos

axios-retry is subtle (which methods, which statuses, exponential delay, Retry-After honoring). Point it at an endpoint where ~half the requests fail with a random 5xx/429 and watch it earn its keep:

import axios from "axios";
import axiosRetry from "axios-retry";

const client = axios.create({ baseURL: "https://mockbird.mockbird.workers.dev/m/demo" });
axiosRetry(client, {
  retries: 5,
  retryDelay: axiosRetry.exponentialDelay,
  retryCondition: (err) =>
    axiosRetry.isNetworkOrIdempotentRequestError(err) ||
    [429, 500, 502, 503, 504].includes(err.response?.status),
});

for (let i = 0; i < 5; i++) {
  const r = await client.get("/products", { params: { mock_chaos: 0.5, limit: 1 } });
  console.log(i, r.status);   // all 200 โ€” the retries absorbed the injected failures
}

mock_chaos=0.5 fails that fraction of requests with a random status from {500, 502, 503, 504, 429}; injected failures carry an x-mockbird-chaos: injected header, and chaos-failed writes are not applied โ€” so it's safe to point retrying write logic at it too. For deterministic 429 testing (fixed window, real Retry-After and x-ratelimit-* headers), use ?mock_ratelimit=N.

6. A pagination helper you can actually test

async function allRecords(resource, limit = 10) {
  let page = 1, out = [], total = Infinity;
  while (out.length < total) {
    const r = await fetch(`${BASE}/${resource}?page=${page}&limit=${limit}`);
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    total = Number(r.headers.get("x-total-count"));
    out = out.concat(await r.json());
    page++;
  }
  return out;
}

console.assert((await allRecords("products")).length === 30);   // 3 real round-trips of 10

There's also opaque cursor pagination (?cursor=) if the API you're mimicking paginates that way.

7. node:test: pin each test to a frozen snapshot

Shared mutable test data is how suites go flaky. Mockbird lets you save named snapshots of a project's entire dataset, then answer any GET from a snapshot โ€” read-only, live data untouched โ€” by sending one header. Set the scenarios up once:

ADMIN='x-admin-key: YOUR_KEY'
P=https://mockbird.mockbird.workers.dev/api/projects/abc123

curl -X POST $P/snapshots -H "$ADMIN" -H 'content-type: application/json' -d '{"name":"baseline"}'
curl -X POST $P/resources/users -H "$ADMIN" -H 'content-type: application/json' -d '{"seed":0}'   # wipe
curl -X POST $P/snapshots -H "$ADMIN" -H 'content-type: application/json' -d '{"name":"empty"}'
curl -X POST $P/snapshots/baseline/restore -H "$ADMIN"    # put live data back

Then each test pins the scenario it wants โ€” with the built-in node:test runner, zero dependencies. No beforeEach resets, no ordering constraints, and parallel test files can't race each other because nobody mutates anything:

// api.test.mjs โ€” run with: node --test
import { test } from "node:test";
import assert from "node:assert/strict";

const BASE = "https://mockbird.mockbird.workers.dev/m/abc123";

function api(snapshot = "baseline") {
  return (path) =>
    fetch(`${BASE}${path}`, { headers: { "X-Mockbird-Snapshot": snapshot } });
}

test("user list (baseline: always 5)", async () => {
  const r = await api()("/users");
  assert.equal((await r.json()).length, 5);
});

test("empty state", async () => {
  const r = await api("empty")("/users");
  assert.deepEqual(await r.json(), []);
  assert.equal(r.headers.get("x-total-count"), "0");
});

We ran exactly this file before publishing: pass 2, fail 0, duration_ms 501. Want to try pinning with zero setup? The public demo ships with built-in empty and edge-cases snapshots (100-char names, unicode, price 0, HTML-ish strings):

await (await fetch("https://mockbird.mockbird.workers.dev/m/demo/products?mock_snapshot=empty")).json()
// โ†’ []

The same trick scales per-scenario โ€” edge-cases, bug-repro-1234 โ€” see the deterministic test data guide.

8. Already on MSW? Eject a handlers file

If part of your suite should stay in-process (offline CI, zero latency), you don't have to choose. GET /m/<project>/msw.js generates a self-contained MSW v2 handlers module with your current data baked in โ€” CRUD, filters, search, pagination with X-Total-Count, all in memory:

curl -o msw.js https://mockbird.mockbird.workers.dev/m/demo/msw.js
# import { setupServer } from 'msw/node'
# import { handlers } from './msw.js'
# setupServer(...handlers).listen()

One dataset, two consumption modes: hosted URL for integration tests and teammates, generated handlers for unit tests. Details in the MSW guide.

9. Honest comparison

The in-process tools are genuinely good, and for pure unit tests they're usually the better choice. The split:

nockaxios-mock-adapterundici MockAgentMSWMockbird
What it isIntercepts http/https module callsStubs one axios instanceBuilt-in undici/fetch interceptorService-worker-style interceptorHosted mock API
Works offline / zero latencyโœ”โœ”โœ”โœ”โœ– (real network)
Works with native fetchPartial (undici needs extra setup)โœ– axios onlyโœ”โœ”โœ” (it's just a URL)
Reachable by teammates, frontend, CI, other servicesโœ– one processโœ– one processโœ– one processโœ– one processโœ” one https URL
Real timeouts / retry-after / slow responsesSimulated at bestSimulatedSimulatedSimulatedโœ” mock_delay/mock_ratelimit
CRUD, filters, pagination, searchYou write every stubYou write itYou write itYou write handlersBuilt in
State persists across processes/runsโœ–โœ–โœ–โœ–โœ” (+ snapshots)
Verify "was this called?"โœ” its superpowerโœ” historyโœ” assertionsโœ” handlersPartial: request inspector
Free tierfree (library)free (library)free (built-in)free (library)free: 20 projects, 10k req/project/day

Use both: keep nock or MockAgent for millisecond-fast unit tests, and point integration tests, scripts, teaching materials and not-yet-built-backend work at a hosted URL. (Or generate the in-process half from the hosted one.)

10. Ship day

Mockbird follows plain REST conventions, so switching to the real backend is one base-URL change (const BASE = process.env.API_URL). Hand your backend team the live contract while they build it: https://mockbird.mockbird.workers.dev/m/abc123/openapi.json, a Postman collection, or generated TypeScript types (?format=zod for Zod schemas) for compile-time safety against the same shapes.

Also mocking for a frontend? The same project feeds React, Vue, Angular, Next.js, SvelteKit, React Native, Flutter and Android at once โ€” one dataset for the whole team. Python, Go, Java, Ruby, PHP, Rust, Elixir or C#/.NET service in the mix too? Same drill. Docs โ†’
โšก Skip the terminal: this link creates a live, seeded backend (products, orders, customers, reviews) in the dashboard โ€” real URL, no signup. Or import your own OpenAPI spec, db.json, CSV, Postman collection, or HAR and mock your exact shapes.