Cypress is a great choice of tool for writing e2e UI automation tests for web applications. One of things you'll invariably want to do when writing tests is stub the dependencies of your system, so that you can isolate the SUT and allow for performant test runs. If you're testing a purely client side code-base, like React, then the built-in cy.intercept might do the job. This will intercept calls made from the browser to downstream APIs and allows you to stub those calls before it leaves the client. However for a Next.js application that includes server side data fetching (e.g. during SSR) or where you have implemented Next.js APIs (e.g. for "backend for frontend" / "edge" APIs) that you want to include as part of the "system under test", you need another option. The way the Cypress Network Requests documentation reads, it seems like the only choices are mocking in the browser using cy.intercept, or spinning up your entire dependency tree - but there is a 3rd option - mocking on the server. Mocking server side calls isn't a new paradigm if you're used to automation testing C# code or have used other UI testing frameworks so I won't go into major detail on the topic, but I wanted to write this article particularly for Cypress as the way you interact with the external mocks is different in Cypress. To mock a downstream API, you can spin up stub servers that allow you to interact with them remotely such as WireMock or in this case "mockserver". What you need to achieve will comprise of these steps:
  1. Before the test run - spin up the mock server on the same address/port as the "real" server would run (actually you can change configs too by using a custom environment, but to keep it simple let's just use the default dev setup)
  2. Before the test run - spin up the system under test
  3. Before an "act" (i.e. during "arrange") you want to setup specific stubs on the mock server for your test to use
  4. During an "assert" you might want to verify how the mock server was called
  5. At the end of a test run, stop the mock server to free up the port
In order to orchestrate spinning up the mock server and the SUT, I'd recommend scripting the test execution - which you can read more about here - below shows an example script to achieve this: test-runner.mjs

#!/usr/bin/env node
import { subProcess, subProcessSync } from 'subspawn';
import waitOn from 'wait-on';
const cwd = process.cwd();

// automatically start the mock
subProcess('test-runner', 'npm run start-mock', true);
await waitOn({ resources: ['tcp:localhost:5287'], log: true }, undefined);

// start the SUT
process.chdir('../../src/YourApplicationRoot');
subProcess('test-runner', 'make run', true);
await waitOn({ resources: ['http://localhost:3000'] }, undefined);

// run the tests
process.chdir(cwd);
subProcessSync("npm run cy:run", true);

// automatically stop the mock
subProcess('test-runner', 'npm run stop-mock', true);

process.exit(0);
That suits full test runs and CI builds, but if you're just running one test at a time from your IDE you might want to manually start and stop the mock server from the command line, which you can do by running the "start-mock" and "stop-mock" scripts from the CLI, hence why they have been split out. start-mock.js

const mockServer = require('mockserver-node');

mockServer.start_mockserver({ serverPort: 5287, verbose: true })
  .then(() => {
    console.log('Mock server started on port 5287');
  })
  .catch(err => {
    console.error('Failed to start mock server:', err);
  });
stop-mock.js

const mockServer = require('mockserver-node');

mockServer.stop_mockserver({ serverPort: 5287 })
  .then(() => {
    console.log('Mock server stopped');
  })
  .catch(err => {
    console.error('Failed to stop mock server:', err);
  });
package.json:

"scripts": {
    "start-mock": "node start-mock.js",
    "stop-mock": "node stop-mock.js",
    "test": "node test-runner.mjs",
    "cy:run": "cypress run"
  },
With the mock server and SUT running you can now interact with them during your test run, however in Cypress the way to achieve this is using custom tasks. Below shows an example task file that allows you to create and verify mocks against the mock-server: mockServerTasks.js

const { mockServerClient } = require('mockserver-client');

const verifyRequest = async ({ method, path, body, times = 1 }) => {
  try {
    await mockServerClient('localhost', 5287).verify({
      method: method,
      path: path,
      body: {
        type: 'JSON',
        json: JSON.stringify(body),
        matchType: 'STRICT'
      }
    }, times);
    return { verified: true };
  } catch (error) {
    console.error('Verification failed:', error);
    return { verified: false, error: error.message };
  }
}

const setupResponse = ({ path, body, statusCode }) => {
  return mockServerClient('localhost', 5287).mockSimpleResponse(path, body, statusCode);
};

module.exports = { verifyRequest, setupResponse };
This can then be imported into your Cypress config:

const { defineConfig } = require("cypress");
const { verifyRequest, setupResponse } = require('./cypress/tasks/mockServerTasks');

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', { verifyRequest, setupResponse })
    },
    baseUrl: 'http://localhost:3000'
  },
});
Finally, with the custom tasks registered with Cypress, you can use the mock server in your tests, e.g.:

it('correctly calls downstream API', () => {
    // setup API mock
    cy.task('setupResponse', { path: '/example', body: { example: 'response' }, statusCode: 200 }).should('not.be.undefined');

    // submit the form (custom command that triggers the behaviour we are testing)
    cy.SubmitExampleForm();
    
    cy.contains('Example form submitted successfully!').should('be.visible');

    // verify API call
    cy.task('verifyRequest', {
      method: 'POST',
      path: '/example',
      body: {
         example: 'request'
      },
      times: 1
    }).should((result) => {
      expect(result.verified, result.error).to.be.true;
    });
  });