ScreenshotNeo

BlogHow-to

How to Test APIs with Cypress: Part 2

Choose between cy.request() and cy.intercept(), assert API behavior, seed test data, and combine reliable API and UI tests in Cypress.

By the ScreenshotNeo team4 October 202611 min read

Use cy.request() to call a real API endpoint directly and assert on its response. Use cy.intercept() to observe, wait for, or stub requests made by the application in the browser. They solve different testing problems: cy.request() runs through Cypress’s Node process, outside the browser network proxy, so an intercept cannot spy on or stub it.

This distinction helps you decide whether a test is checking an endpoint’s contract, controlling browser traffic, preparing test data, or verifying that a user workflow persisted a change. This guide follows Cypress’s API Testing guide, request command reference, and intercept command reference.

1. Choose the command that matches the test

Test goal Use What happens
Call an endpoint and check its actual response cy.request() Cypress makes a direct HTTP request. The request does not pass through cy.intercept().
Wait for the app’s request after a user action cy.intercept() with an alias and cy.wait() Cypress observes matching browser traffic and yields request and response details.
Make the UI handle a controlled response cy.intercept() with a static response or handler The matching browser request can receive a stub instead of reaching the server.
Seed or clean server-side test data Usually cy.request(); use cy.task() for Node-side work such as direct database access Prepare a repeatable starting state without using irrelevant UI steps.

Use both in one test when that gives a clearer test: prepare data with a direct request, exercise the real browser workflow, then use another direct request to check persisted state. Keep tests that exercise the real server where server behavior matters; a stubbed response alone does not prove that the live endpoint works.

2. Configure Cypress and make a direct API request

Set baseUrl in the Cypress end-to-end configuration if you want to use relative endpoint paths. A full URL also works without that setting.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
})

A basic spec can assert the status and a field that belongs to the endpoint’s contract:

// cypress/e2e/users-api.cy.js
describe('Users API', () => {
  it('returns users', () => {
    cy.request('GET', '/api/users').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body).to.have.property('results')
      expect(response.body.results).to.be.an('array')
    })
  })
})

Replace the example route and assertions with your API’s documented contract. Avoid asserting incidental values that can change independently of correct behavior. A request response includes properties such as status, body, headers, and duration.

Request signatures and options

Cypress supports cy.request(url), cy.request(url, body), cy.request(method, url), cy.request(method, url, body), and cy.request(options). The method defaults to GET. For example, a JSON creation request can be written as:

cy.request({
  method: 'POST',
  url: '/api/users',
  body: { name: 'Ada Lovelace' },
  headers: { 'x-test-run': 'cypress' },
}).then((response) => {
  expect(response.status).to.eq(201)
  expect(response.body).to.have.property('id')
})

The options object lets you set the URL, method, body, headers, authentication, query parameters, encoding, logging, and response behavior. Consult the current request reference for the complete option list and defaults for your installed Cypress version. The reference documents options including failOnStatusCode, retryOnStatusCodeFailure, retryOnNetworkFailure, followRedirect, qs, auth, form, encoding, gzip, headers, log, and timeout.

  • Relative URL: Cypress uses the visited page’s host if a page has already been visited; before a visit, it uses configured baseUrl. A full URL avoids ambiguity.
  • Expected error responses: By default, a non-2xx or non-3xx status fails the command. Set failOnStatusCode: false when the test is specifically checking a 4xx or 5xx response, then assert the expected status yourself.
  • Authentication: Use request headers or the documented auth option for the API’s actual authentication scheme. Cypress also applies matching cookies to requests and applies response Set-Cookie values to the browser cookie jar. Do not assume an API uses cookie authentication.
  • Query strings and bodies: Use qs for query parameters and body for request payloads where appropriate. Match the content type and serialization expected by your server.
  • Assertions and retries: A cy.request() command does not retry chained assertions. Configure request retry options deliberately for transient transport or status failures, but do not use retries to hide an incorrect expected result.

3. Test status codes, validation, and permissions

Direct requests are useful for backend cases that may be awkward or slow to reach through a form: invalid input, authorization boundaries, and endpoint response contracts.

it('rejects an invalid user payload', () => {
  cy.request({
    method: 'POST',
    url: '/api/users',
    body: { email: 'not-an-email' },
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.eq(422)
    expect(response.body).to.have.property('error')
  })
})

Use the status and response fields your API actually specifies. For authorization tests, create or obtain credentials with the intended permissions and check both the status and any meaningful error shape. Keep secrets out of source control and avoid printing authorization headers or sensitive response bodies into CI logs.

4. Seed data before a test and verify it afterward

A dedicated test-only seed endpoint can establish known data without spending test time navigating setup screens. Use a separate environment and ensure seeded records cannot affect production data. Clean up or create unique records so parallel and repeated runs do not collide.

describe('order checkout', () => {
  beforeEach(() => {
    cy.request('POST', '/test-support/seed-order', {
      reference: 'cypress-order-42',
      status: 'draft',
    })
  })

  it('submits the order and persists the result', () => {
    cy.visit('/orders/cypress-order-42')
    cy.get('[data-cy=submit-order]').click()

    cy.request('/api/orders/cypress-order-42').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body.status).to.eq('submitted')
    })
  })
})

The endpoint names and fields above are illustrative; implement safe test support in your own application. If setup requires Node-side database access rather than an HTTP endpoint, use cy.task() for that work. Keep the browser portion focused on the user behavior under test.

5. Observe or stub browser traffic with cy.intercept()

Register the intercept before the action that triggers the request. Add an alias, wait for it, and assert on the request or response:

it('loads the user list in the browser', () => {
  cy.intercept('GET', '/api/users').as('getUsers')

  cy.visit('/users')
  cy.wait('@getUsers').then((interception) => {
    expect(interception.response.statusCode).to.eq(200)
    expect(interception.response.body.results).to.be.an('array')
  })
})

To control the response for a UI case, supply a static response:

it('shows an empty state when the API returns no users', () => {
  cy.intercept('GET', '/api/users', {
    statusCode: 200,
    body: { results: [] },
  }).as('getUsers')

  cy.visit('/users')
  cy.wait('@getUsers')
  cy.contains('No users yet').should('be.visible')
})

A static response can include statusCode, headers, body, or a fixture. It can also simulate delay, throttling, or a network error. A route handler can inspect or modify the outgoing request, let it reach the real server, inspect the real response, or provide a stub. Intercepts are cleared before each test, so register them per test or in setup hooks.

Matching details that commonly matter

  • Specify the HTTP method when it matters. With no method, a route can match requests of any method.
  • Match the actual URL, including the correct host, path, and query string. Route matcher objects can specify properties such as method, hostname, pathname, path, query, and headers.
  • String route patterns use Cypress glob matching. Escape a literal question mark in a pattern where needed; an unescaped ? can act as a wildcard.
  • For GraphQL, requests often share one URL. Inspect the request body and assign an alias based on the operation you need to wait for.
  • If browser caching serves a resource without a network request, a network-layer intercept may not fire. Check the browser developer tools and test environment cache behavior.

6. Combine API and UI checks without confusing their evidence

A strong workflow can use a real request to prepare state, browser actions to exercise the product, and a final request to verify persistence. An intercept can separately prove that the browser made a request with the expected payload or responded to a controlled failure.

  1. Use cy.request() or cy.task() to create a known precondition.
  2. Register any cy.intercept() aliases before visiting the page or triggering the relevant action.
  3. Drive the user behavior through the browser.
  4. Wait on the aliased browser request instead of adding an arbitrary delay.
  5. Assert the visible result and, when useful, verify persisted state with a direct request.
  6. Clean up test data or use isolated identifiers so repeated and parallel runs stay independent.

Keep the purpose of each assertion clear. A stubbed response checks how the UI behaves given that response. A direct request checks the server endpoint. A browser-driven request checks the app’s browser workflow. A direct request bypasses browser CORS enforcement, so it does not establish that a browser is permitted to make a cross-origin request.

7. Troubleshoot common failures

Symptom Likely cause Fix
cy.request() goes to the wrong host The relative path resolved against the visited host or configured baseUrl. Set the intended baseUrl, use a full URL, or check whether an earlier cy.visit() changed the host context.
A 4xx/5xx response fails before your assertion failOnStatusCode defaults to failing on non-2xx/3xx responses. Set failOnStatusCode: false for the negative case, then assert the expected status and error body.
cy.intercept() does not catch cy.request() The direct request runs through Cypress’s Node process outside the browser proxy. Assert the direct response from the cy.request() chain. Use an intercept only for browser-originated traffic.
An alias wait times out The intercept may be registered too late, use the wrong method or URL, or the app may not have made a network request. Register before the triggering action; verify method, host, path, query, and app behavior in the Command Log and browser developer tools.
An intercept does not fire for a cached resource The browser served the resource from cache without sending it over the network. Check browser tools and configure the test server’s cache behavior for the test environment where appropriate.
The browser shows a CORS error while the direct request passes cy.request() bypasses browser CORS enforcement. Test the actual browser flow when cross-origin browser policy is part of the requirement; do not treat direct-request success as proof of browser access.
A request fails intermittently and a fixed delay seems to help The test is racing the app’s request or depending on variable network timing. Alias the matching intercept before the action and use cy.wait('@alias'). Check timeouts and server health if the response itself is slow.
Setup data is missing or tests interfere with each other Seed setup is not awaited, data is shared, or cleanup is incomplete. Chain setup as Cypress commands, use unique test identifiers or isolated records, and clean up safely.
Assertions pass locally but logs reveal sensitive data Request and response details can appear in the Command Log or CI artifacts. Use test credentials, avoid secrets in examples, and limit or disable logging for sensitive commands where suitable.

The Cypress Command Log provides request and response details for debugging. Recorded CI runs may also expose command details through Test Replay when enabled. Review logs before sharing them because headers and bodies can contain secrets or personal data.

8. Performance, reliability, and cost

  • Keep setup direct: Seed data through an API or task when the setup UI is not part of the behavior being tested. This reduces unrelated browser steps.
  • Wait on events: Use an aliased intercept to synchronize on browser requests. Arbitrary sleeps add time and can still be too short or unnecessarily long.
  • Keep route matching narrow: Match the intended endpoint and method. Broad interception can add noise and make the test harder to understand.
  • Balance stubs and real services: Stubs make UI cases controlled and repeatable, but they do not verify the live server contract. Include direct endpoint coverage where that contract matters.
  • Handle transient failures deliberately: Set appropriate timeouts and retries for your environment, and make retries visible in the test design. A retry should address plausible transient transport failures, not conceal a broken contract.
  • Control data and credentials: Isolate test environments, use non-production data, and prevent credentials or private response data from leaking into logs.
  • Budget: These commands are part of Cypress tests; the dossier provides no per-request Cypress price or benchmark. Account for CI runtime, hosted-runner usage, and any API costs imposed by your own service. Avoid adding network calls that do not increase coverage.

9. Or skip the browser setup

If your task is to capture a website screenshot for a visual check, report, or agent workflow, ScreenshotNeo provides a website screenshot API and MCP server. Cypress remains useful for testing your own browser application and API behavior; ScreenshotNeo is an alternative to try first when the deliverable is a clean website capture.

One GET request returns an image or PDF. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' })
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
  • Cookie banners and consent overlays, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.

Sign up for 1,000 free screenshots a month, with no card required.

10. FAQ

Can I use cy.request() before cy.visit()?

Yes. Before visiting a page, a relative request uses the configured baseUrl. A full URL also works.

Can I use cy.intercept() to mock an API for every test?

You can register stubs in a test or setup hook. Intercepts are cleared before each test, so configure the routes again for each test that needs them.

Should every API test also visit the UI?

No. Test endpoint contracts directly when the browser is not part of the behavior. Use browser-driven tests for behavior that depends on the app’s interaction with the endpoint.

Does a successful cy.request() prove a user’s browser can reach that API?

No. The command bypasses browser CORS enforcement. Exercise a browser flow to check browser-specific behavior.

References