Cypress Intercept: How to Mock API Calls and Network Requests
Network requests are at the core of every modern web application — and they're one of the biggest sources of test instability. A backend that's slow, unavailable, or returns unexpected data can fail tests that have nothing to do with the code you're testing. cy.intercept() gives you full control over network requests in Cypress: stub responses, inject delays, simulate errors, and spy on what your app sends to the server.
What Is cy.intercept()?
cy.intercept() intercepts network requests made by the browser during a test. You can:
- Stub — return a fake response instead of hitting the real server
- Spy — let the request through but assert on it afterward
- Delay — simulate slow network conditions
- Abort — simulate connection failures
It replaces the older cy.route() API, which required cy.server(). cy.intercept() requires no setup and works with fetch, XMLHttpRequest, and any library that uses them.
Basic Stub
Return a fixed JSON response:
cy.intercept('GET', '/api/users', {
body: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.get('[data-cy="user-item"]').should('have.length', 2);The .as('getUsers') alias lets you reference the intercept in cy.wait() and assertions.
Matching Requests
You can match by method, URL, path, or query parameters:
// Exact URL
cy.intercept('GET', '/api/users', { body: [] });
// Glob pattern
cy.intercept('GET', '/api/users/**', { body: { name: 'Alice' } });
// Regex
cy.intercept('GET', /\/api\/users\/\d+/, { body: { id: 1, name: 'Alice' } });
// With query parameters
cy.intercept('GET', '/api/search?q=cypress', { body: [] });
// Route matcher object
cy.intercept({
method: 'POST',
url: '/api/orders',
headers: { 'Content-Type': 'application/json' },
});Using Fixtures
Fixtures are JSON files stored in cypress/fixtures/. They keep test data out of your test files and make it easy to reuse across tests.
Create cypress/fixtures/users.json:
[
{ "id": 1, "name": "Alice Smith", "email": "alice@example.com", "role": "admin" },
{ "id": 2, "name": "Bob Jones", "email": "bob@example.com", "role": "member" }
]Use it in a test:
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/admin/users');
cy.wait('@getUsers');
cy.get('[data-cy="user-row"]').should('have.length', 2);
cy.contains('Alice Smith').should('be.visible');
cy.contains('admin').should('be.visible');You can also load fixtures dynamically:
cy.fixture('users.json').then((users) => {
cy.intercept('GET', '/api/users', { body: users });
});Spying on Requests (Without Stubbing)
To let the request hit the real server but assert on what was sent:
cy.intercept('POST', '/api/orders').as('createOrder');
cy.get('[data-cy="checkout"]').click();
cy.wait('@createOrder').then((interception) => {
expect(interception.request.body).to.deep.equal({
items: [{ id: 1, quantity: 2 }],
total: 29.99,
});
expect(interception.response?.statusCode).to.eq(201);
});This is valuable for verifying that your app sends the correct payload — without mocking the response.
Modifying Real Responses
Instead of fully stubbing, you can intercept and modify the real response:
cy.intercept('GET', '/api/config', (req) => {
req.continue((res) => {
// Inject a feature flag
res.body.featureFlags.newDashboard = true;
});
});This is useful for testing feature flags, injecting edge-case data, or testing how your app handles specific response fields without mocking the entire endpoint.
Simulating Error Responses
Test how your app handles API errors:
// 500 Internal Server Error
cy.intercept('GET', '/api/dashboard', {
statusCode: 500,
body: { message: 'Internal server error' },
}).as('dashboardError');
cy.visit('/dashboard');
cy.get('[data-cy="error-banner"]').should('be.visible');
cy.get('[data-cy="error-banner"]').should('contain', 'Something went wrong');// 404 Not Found
cy.intercept('GET', '/api/users/999', { statusCode: 404 });
cy.visit('/users/999');
cy.get('[data-cy="not-found"]').should('be.visible');// 401 Unauthorized — test session expiry
cy.intercept('GET', '/api/me', { statusCode: 401 });
cy.visit('/dashboard');
cy.url().should('include', '/login');
cy.get('[data-cy="session-expired"]').should('be.visible');Simulating Network Failures
Test offline behavior or connection timeouts:
// Force network error (connection refused)
cy.intercept('POST', '/api/submit', { forceNetworkError: true });
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="network-error"]').should('be.visible');// Simulate slow response (1500ms delay)
cy.intercept('GET', '/api/report', (req) => {
req.reply((res) => {
res.delay = 1500;
res.send({ data: [] });
});
});
cy.visit('/report');
cy.get('[data-cy="loading-spinner"]').should('be.visible');
cy.get('[data-cy="report-data"]').should('be.visible');Asserting on Request Headers and Body
cy.intercept('POST', '/api/feedback').as('submitFeedback');
cy.get('[data-cy="feedback-input"]').type('Great product!');
cy.get('[data-cy="submit-feedback"]').click();
cy.wait('@submitFeedback').its('request.body').should('deep.equal', {
message: 'Great product!',
rating: null,
});
cy.wait('@submitFeedback').its('request.headers').should('have.property', 'authorization');Intercepting Multiple Requests
Cypress intercepts each request that matches — you can count how many times an endpoint is called:
let callCount = 0;
cy.intercept('GET', '/api/search*', (req) => {
callCount++;
req.reply({ body: [] });
});
// Type each character — should debounce to 1 request
cy.get('[data-cy="search"]').type('hello');
cy.then(() => {
expect(callCount).to.eq(1);
});Dynamic Intercepts Based on Request Data
cy.intercept('POST', '/api/login', (req) => {
const { email } = req.body;
if (email === 'blocked@example.com') {
req.reply({ statusCode: 403, body: { message: 'Account suspended' } });
} else if (email === 'slow@example.com') {
req.reply({ delay: 2000, statusCode: 200, body: { token: 'abc' } });
} else {
req.reply({ statusCode: 200, body: { token: 'valid-token' } });
}
});This single intercept handles multiple scenarios based on request content — useful for shared test setup.
Intercept in beforeEach
For tests that always need mocked data, set up intercepts in beforeEach:
describe('Dashboard', () => {
beforeEach(() => {
cy.intercept('GET', '/api/dashboard', { fixture: 'dashboard.json' }).as('getDashboard');
cy.intercept('GET', '/api/notifications', { body: [] }).as('getNotifications');
cy.visit('/dashboard');
cy.wait('@getDashboard');
});
it('shows stats from API', () => {
cy.get('[data-cy="total-users"]').should('contain', '1,240');
});
it('shows zero notification badge when empty', () => {
cy.get('[data-cy="notification-count"]').should('not.exist');
});
});Common Mistakes
Using cy.wait('@alias') without the @ prefix:
// Wrong
cy.wait('getUsers');
// Correct
cy.wait('@getUsers');Not waiting before asserting: Without cy.wait('@alias'), your assertion may run before the response arrives.
Overriding intercepts mid-test: Cypress uses the most recent matching intercept. If you set up two intercepts for the same URL, the second one wins. Use the times option if you need one intercept to apply only once:
cy.intercept('GET', '/api/users', { body: [] }).as('emptyList');
cy.intercept({ method: 'GET', url: '/api/users', times: 1 }, { body: [] });Summary
cy.intercept() gives you precise control over network behavior in your tests:
- Stub responses with fixed JSON or fixtures to isolate frontend logic
- Spy on real requests to assert payloads and headers
- Modify real responses to inject edge-case data
- Simulate errors, slow responses, and network failures
- Count requests to verify debouncing and caching behavior
This makes your tests fast, reliable, and deterministic — regardless of what the backend is doing.