cy.intercept, a hosted mock, or bothFirst, the part most "mock API for Cypress" posts skip: cy.intercept is genuinely excellent. It stubs at the network layer of the browser under test, works offline, answers instantly, and its alias + cy.wait('@alias') pattern is the cleanest way anywhere to assert that your app made a request. If everything that needs the fake data is the page Cypress is driving, use cy.intercept and stop reading.
But cy.intercept has a boundary that bites people, and it's right in the Cypress docs: it only sees requests made by the page. Requests you make with cy.request() — the tool Cypress itself recommends for API testing, seeding, and login shortcuts — go out from the Cypress Node process and are never intercepted. §3 below is a runnable test that proves it. So the moment you want API-level tests, seeded state, or data that anything other than the browser-under-test can see, you need a real URL. That's what a hosted mock is for — and the two tools compose nicely.
Every snippet below was run verbatim before publishing (Cypress 15.19, Node 22, Electron headless — 6 tests, all passing, including the 3-second real-latency run in §5).
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' -d '{"name":"cy-demo","preset":"ecommerce"}'
# → {"id":"abc123","adminKey":"...","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123", ...}
One curl → a seeded multi-resource CRUD API (products, orders, customers, reviews) with filtering, sorting, pagination, relations and CORS. Prefer a UI? One click creates the same project in the dashboard. The snippets below use the public demo project so they run with zero setup.
cy.requestCypress is a perfectly good API test runner — cy.request plus a mock URL means your API specs run before the backend exists, and writes are real state you can read back in the next step (something a static cy.intercept fixture can't do without you hand-maintaining the bookkeeping):
// cypress/e2e/api.cy.js
const API = "https://mockbird.mockbird.workers.dev/m/demo";
describe("products API (hosted mock)", () => {
it("lists seeded products with pagination", () => {
cy.request(`${API}/products?_page=2&_limit=5`).then((res) => {
expect(res.status).to.eq(200);
expect(res.body).to.have.length(5);
expect(res.headers).to.have.property("x-total-count", "30"); // real header
});
});
it("create → read back → delete (real state)", () => {
cy.request("POST", `${API}/products`, {
name: "Cypress Test Lamp",
price: 49.5,
}).then((create) => {
expect(create.status).to.eq(201);
const id = create.body.id;
cy.request(`${API}/products/${id}`).its("body.name")
.should("eq", "Cypress Test Lamp");
cy.request("DELETE", `${API}/products/${id}`).its("status").should("eq", 200);
});
});
it("error state is one query param", () => {
cy.request({
url: `${API}/products?mock_status=503`,
failOnStatusCode: false,
}).its("status").should("eq", 503);
});
});
Config is minimal — no baseUrl needed for absolute URLs:
// cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: { supportFile: false, video: false, screenshotOnRunFailure: false },
});
Here's the test. We register an intercept that would replace the products list with an empty array and count how often it fires — then hit the same URL with cy.request:
// cypress/e2e/intercept-blindspot.cy.js
const API = "https://mockbird.mockbird.workers.dev/m/demo";
describe("cy.intercept blind spot", () => {
it("intercept handler never fires for cy.request", () => {
let hits = 0;
cy.intercept(`${API}/products*`, (req) => {
hits += 1;
req.reply({ statusCode: 200, body: [] });
});
cy.request(`${API}/products?_limit=3`).then((res) => {
// Real data came back — the stub was NOT applied.
expect(res.body).to.have.length(3);
expect(hits).to.eq(0);
});
});
});
Result: passing. Three real products, hits === 0. This isn't a bug — the Cypress docs state that cy.request is never stubbed. The practical consequence: any test strategy built purely on cy.intercept covers only the browser page. Your seeding commands, API assertions, and login-by-request shortcuts all talk to whatever URL is really there — so make it a mock URL you control.
Because the mock itself understands simulation params, slow-API and broken-API tests don't need an intercept at all — the page under test experiences real latency end-to-end:
// cypress/e2e/ui.cy.js — app fetches `${API}/products?_limit=5&mock_delay=2000`
describe("loading state", () => {
it("shows the spinner while the API is slow, then real data", () => {
cy.visit("http://localhost:8123/?delay=2000");
cy.get("#spinner").should("be.visible"); // still loading at t≈0
cy.get("#list li", { timeout: 6000 }).should("have.length", 5);
cy.get("#spinner").should("not.be.visible");
});
});
Ran in 3.1s — the two seconds of spinner are real. The same trick covers the whole error matrix: ?mock_status=503, ?mock_status=401, ?mock_chaos=0.5 for flaky-API retry tests, ?mock_jitter=800 for realistic variance. QA can reproduce any of these states in a normal browser tab by pasting the URL — no Cypress required, which is exactly what an in-run intercept can never give you.
Empty list, edge cases, bug repro — instead of maintaining fixture files per scenario, save named snapshots once and pin any request to one, read-only, without touching live data:
// cypress/e2e/scenarios.cy.js
const API = "https://mockbird.mockbird.workers.dev/m/demo";
describe("empty-state scenario", () => {
it("same URL serves the pinned empty snapshot", () => {
cy.request({
url: `${API}/products`,
headers: { "X-Mockbird-Snapshot": "empty" },
}).then((res) => {
expect(res.status).to.eq(200);
expect(res.body).to.deep.eq([]);
});
// live data untouched
cy.request(`${API}/products?_limit=1`).its("body").should("have.length", 1);
});
});
For page fetches, add the header app-side in test builds, or append ?mock_snapshot=empty to the URL. Specs running on different machines (Cypress parallelization, CI shards) can each pin a different scenario against one shared project — no restore races, no per-shard seed scripts.
cy.intercept + fixtures | Hosted mock (Mockbird) | |
|---|---|---|
| Browser-page stubbing | Excellent — instant, offline, per-test isolation | Real network round-trip (~50–200ms) |
| Asserting "app made this call" | Best in class (cy.wait('@alias')) | Request inspector (after the fact) |
cy.request / API tests | ✗ never intercepted (§3) | ✔ it's a real URL |
| State across test steps | Manual fixture bookkeeping | ✔ real CRUD persistence |
| Backend doesn't exist yet | You hand-write every response | ✔ schema + seeded data in one curl |
| Seen by teammates / QA / mobile / curl | ✗ exists only inside the run | ✔ shareable URL |
| Slow / flaky API simulation | Static delay per stub | ✔ mock_delay, mock_chaos, mock_jitter per request |
| Data scenarios | Fixture file per scenario | ✔ named snapshots, pinned per request |
| Works offline | ✔ | ✗ needs network |
The pattern that works: point the app's API_URL at the hosted mock (env var in your test build), let cy.request seed and assert against it, and keep cy.intercept for the two things it's best at — spying (cy.wait('@alias') to assert a call happened with the right payload) and surgical overrides (force one specific response mid-flow while everything else stays real). Stub the exception, host the rule.
demo project are capped daily. Create your own project for real work — free, no signup: one click or one curl (§1). Related: Playwright · Jest · Vitest · deterministic test data · loading & error states.