ScreenshotNeo

BlogHow-to

How to Test APIs with Cypress

Use Cypress to call API endpoints directly, observe or stub browser requests, and connect API checks with UI tests. Includes runnable examples and troubleshooting.

By the ScreenshotNeo team4 October 202611 min read

Use cy.request() to call a running API directly and assert on its response. Use cy.intercept() to observe, wait for, or stub requests initiated by the application in the browser. Use cy.task() for Node-side setup such as database access or file work. These commands solve different problems: an API call made with cy.request() does not pass through cy.intercept().

Cypress API tests run in the end-to-end testing type. The examples below use JavaScript and Cypress’s documented commands. Set the base URL and credentials for your own test environment, and check the defaults against the Cypress version in your project.

1. Configure Cypress for API tests

Set baseUrl in Cypress configuration so requests can use paths such as /users. For example, in cypress.config.js:

const { defineConfig } = require('cypress')

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

Use the URL of the API or application environment that the tests should reach. A baseUrl is optional: you can pass a complete URL to cy.request(). If you use a relative URL without a configured base URL, Cypress can resolve it against the host from a page already visited with cy.visit().

Create a spec file in your project’s Cypress end-to-end specs directory, for example cypress/e2e/api/users.cy.js. Cypress can also be configured to use a different spec directory.

2. Write a basic endpoint test

This test calls a real endpoint directly, then checks its status and response shape:

describe('GET /users', () => {
  it('returns a list of users', () => {
    cy.request('GET', '/users').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body.results).to.be.an('array')
      expect(response.body.results).to.have.length.greaterThan(1)
    })
  })
})

cy.request() yields a response object. Commonly useful fields include status, body, headers, and duration. Assert on the contract your service promises, not incidental fixture contents that may change. A timing assertion can help detect a specific regression, but choose a threshold that fits the test environment and avoid making it a substitute for a performance test.

Cypress supports these request forms:

  • cy.request(url)
  • cy.request(url, body)
  • cy.request(method, url)
  • cy.request(method, url, body)
  • cy.request(options)

The options form is useful when a request needs headers, query parameters, cookies, or behavior overrides.

3. Test methods, payloads, and error responses

Use a request body for methods such as POST and PUT. Object and Boolean bodies are serialized as JSON and receive an application/json content type. String bodies are sent as-is, without Cypress automatically adding that content type.

describe('POST /users', () => {
  it('creates a user', () => {
    cy.request('POST', '/users', {
      name: 'Ada Lovelace',
      email: 'ada@example.test',
    }).then((response) => {
      expect(response.status).to.eq(201)
      expect(response.body).to.include({
        name: 'Ada Lovelace',
        email: 'ada@example.test',
      })
      expect(response.body.id).to.exist
    })
  })
})

For an expected non-success response, set failOnStatusCode: false so Cypress returns the response for your assertions instead of failing the command automatically:

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

This pattern is useful for validation failures, authorization boundaries, rate limits, and pagination edge cases when those behaviors are part of your API contract. Assert on the exact status and useful error fields. A test that only asserts “not 200” can pass for the wrong failure.

4. Send authenticated requests and reuse login state

Pass authorization data through request options. Keep environment-specific hosts and secrets out of committed test code; store them in Cypress environment configuration or your CI secret store.

it('returns the current account for an authenticated user', () => {
  cy.request({
    method: 'GET',
    url: '/account',
    headers: {
      Authorization: `Bearer ${Cypress.env('API_TOKEN')}`,
    },
  }).then((response) => {
    expect(response.status).to.eq(200)
    expect(response.body).to.have.property('id')
  })
})

Cypress sends matching browser cookies with cy.request() and reflects response Set-Cookie values into the browser cookie jar. This lets API setup and later UI activity share login state. Direct requests are not browser traffic: browser CORS and same-origin restrictions do not apply, and the request does not show in the browser’s Network tab.

For repeated setup, define a custom command that centralizes the API prefix and authorization headers. Keep the command focused so a test still makes clear what state it creates. Cypress’s API testing guide documents this approach.

5. Choose between cy.request(), cy.intercept(), and cy.task()

Need Use Where the work happens Real backend?
Call an endpoint and assert on its response cy.request() Cypress’s Node process Yes, unless the endpoint is itself a stubbed test service
Observe, wait for, or stub an app request cy.intercept() Browser application’s network traffic Pass-through reaches the backend; a stub does not
Run database, file, or other Node-side setup cy.task() Node process through Cypress’s task interface Depends on the task

Use cy.intercept() when the behavior under test is a browser action and its resulting request or response. Register the intercept before the action that triggers it:

it('loads users in the UI', () => {
  cy.intercept('GET', '/api/users').as('getUsers')
  cy.visit('/users')
  cy.wait('@getUsers').then(({ request, response }) => {
    expect(request.method).to.eq('GET')
    expect(response.statusCode).to.eq(200)
  })
})

To control an application state deterministically, provide a stubbed response:

it('shows an empty state when there are 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 stub is useful for an edge case that is hard to create or for isolating UI behavior. Use a real response when the test needs to verify the integrated backend. A suite can use both strategies for different cases.

cy.task() is appropriate when setup needs direct database access or Node-side file I/O. It is not a replacement for a public API contract check: use cy.request() when the API endpoint itself is what you want to exercise.

6. Combine API setup with UI checks

API calls can establish or inspect state around a UI test. This avoids spending UI steps on setup while still testing the user-facing interaction:

describe('user settings', () => {
  it('updates a name and persists it', () => {
    cy.request('POST', '/test/reset-user', {
      name: 'Ada Lovelace',
    }).then((seedResponse) => {
      expect(seedResponse.status).to.eq(200)
    })

    cy.visit('/settings')
    cy.get('[name="name"]').clear().type('Ada Byron')
    cy.contains('button', 'Save').click()

    cy.request('/api/account').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body.name).to.eq('Ada Byron')
    })
  })
})

Replace the example reset endpoint and selectors with ones from your app. Keep test-only reset routes unavailable in production. A useful pattern is to authenticate through HTTP and continue in the UI, authenticate through the UI and query an authenticated endpoint, or seed through HTTP, operate through the interface, and verify persistence through HTTP.

7. Request options and important defaults

Use the documented request options to tailor the test to the endpoint and response you need to inspect. Consult the Cypress cy.request() reference for the complete option list for your installed version.

Option or behavior What to know
url, method, body Describe the endpoint call. Relative paths use baseUrl when configured.
headers, qs, auth Supply request headers, query string values, or authentication details as appropriate. Avoid hardcoding secrets.
failOnStatusCode Defaults to true; set to false when a non-2xx/3xx response is the expected subject of the test, then assert explicitly.
followRedirect Redirects are followed by default. Disable when the redirect response or its Location behavior is what you need to test.
timeout Overrides the request timeout for that call. cy.request() uses responseTimeout, not defaultCommandTimeout.
Transient network errors Cypress retries them by default, up to four times according to its API testing guide. Status-code failures are not retried unless configured.
Response cookies Response Set-Cookie values are reflected into the browser cookie jar, enabling shared auth state.

Defaults can change across Cypress releases. Verify them against the version your project pins, especially when a test depends on retry, redirect, or timeout behavior.

8. Organize larger API test suites

  • Group related requests: Put related endpoint checks in a spec so Cypress’s browser startup cost is amortized. Cypress starts a browser per spec file; avoid a separate spec for every small request without a reason.
  • Keep test data controlled: Seed or reset state through a test API or a cy.task() as appropriate. Do not rely on another test having run first.
  • Use fixtures for large payloads: Keep bulky request and response examples in fixtures, while leaving the assertion intent visible in the spec.
  • Use aliases for values needed later: Cypress commands are queued; avoid assigning command results to ordinary variables as if they were synchronous. Use .then(), aliases, or fixture flows.
  • Keep real and stubbed coverage intentional: Real responses verify integration. Stubs make difficult states deterministic. Label or structure the tests so readers know which behavior each test covers.
  • Cover meaningful boundaries: Include malformed input, missing credentials, permission checks, empty and last-page pagination, and documented rate-limit behavior where relevant to your API.

9. Performance and reliability

Direct API checks avoid page rendering and simulated interactions, so they can focus feedback on endpoint behavior. They do not replace UI tests for presentation and user interaction. Keep both layers: use API calls for efficient state setup or backend contract checks, then use the browser for behavior that only the application UI can validate.

Reduce avoidable suite cost by grouping related checks into specs and using API setup rather than long UI setup flows when the UI setup itself is not under test. Avoid overly strict duration assertions: network variability, shared CI resources, and service load can make a single request fluctuate. Use a stable test environment and thresholds that reflect a real requirement.

For reliable results, use isolated test data, reset state between cases when needed, and make retry behavior explicit in the test’s intent. Network retries can mask a transient environment issue; they do not make an unhealthy dependency correct. A status assertion should still establish the expected API contract.

10. Troubleshooting

Symptom Likely cause Fix
Expected 4xx or 5xx response fails the command failOnStatusCode defaults to true. Set failOnStatusCode: false and assert the intended status and error body.
Relative request URL reaches the wrong host baseUrl is missing or points to another environment; without it, Cypress may use the host from a prior visit. Set the intended baseUrl or pass a complete URL.
cy.intercept() does not see a cy.request() cy.request() runs outside browser traffic and bypasses the intercept. Assert directly on the cy.request() response. Use cy.intercept() for a request caused by the application.
Intercept sees no request after the page loads The intercept was registered after the action, the route matcher is wrong, or a cached response avoided the network layer. Register the intercept first, verify method and URL matching, and consider disabling cache headers in the test environment when cache prevents a network request.
Request times out The service is unavailable, the URL is wrong, or the configured response timeout is too short for the environment. Confirm the endpoint is reachable, then set a suitable per-request timeout or project responseTimeout. Do not just raise it to hide a service failure.
String request body is rejected or parsed incorrectly Strings are sent as-is and do not automatically receive a JSON content type. Send an object for JSON serialization or set the appropriate Content-Type header and encode the string as the endpoint expects.
Redirect test returns the destination response Redirects are followed by default. Set followRedirect: false and assert on the redirect status and Location header.
UI test has no authenticated session after API login The API call did not establish a browser cookie, or the app uses a different auth mechanism such as a token stored by frontend code. Check the login response and cookie behavior. If auth is held in frontend storage, set up that state through the app’s supported test path or authenticate through the UI.
Suite depends on test order One test creates state that another silently expects. Make each test seed its own data or use explicit suite setup and cleanup with isolated records.

11. Or skip the browser setup

If your goal is to capture a website screenshot as part of developer documentation, visual checks, or an AI workflow, ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. It is separate from Cypress API testing: use Cypress for endpoint and application tests, and use the screenshot API when you need an image or PDF of a page.

One GET request returns a screenshot. See the ScreenshotNeo API documentation for request options.

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}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
await Bun.write('shot.webp', res)

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

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

12. FAQ

Can Cypress test an API without opening a page?

Yes. A spec can use cy.request() directly against an API endpoint without first visiting a page.

Should every API test use a real backend?

No. Use real responses for backend contract and integration coverage. Stub browser requests when a deterministic UI state or difficult edge case is the target.

Does cy.request() enforce browser CORS rules?

No. It is sent from Cypress’s Node process, outside normal browser same-origin restrictions.

Can cy.intercept() inspect a cached browser response?

A response served from cache may not reach the network layer, so it may not trigger an intercept. A test environment can disable cache headers when the network request itself must be observed.

Primary references