Resource library

QA How-To

How to Use Cypress cy.request for API (2026)

Learn cypress cy.request for API testing with practical TypeScript patterns for auth, assertions, test data, negative cases, redirects, and CI reliability.

19 min read | 2,898 words

TL;DR

Call cy.request with a URL, method and URL, method, URL and body, or an options object. Use the yielded response to assert status, headers, body, and duration, and use direct API calls to create predictable preconditions before testing the UI.

Key Takeaways

  • Use cy.request for direct HTTP calls, API assertions, authentication, and test data setup without driving the browser UI.
  • Choose the options object when a request needs headers, query parameters, form encoding, redirect control, retries, or negative-status handling.
  • Assert the status, headers, and business-critical body fields instead of checking only for HTTP 200.
  • Set failOnStatusCode to false only when a non-2xx or non-3xx response is the expected behavior under test.
  • Remember that cy.request is not intercepted by cy.intercept because it does not originate from the application browser request layer.
  • Build idempotent seed and cleanup helpers so parallel CI jobs do not depend on shared, mutable data.
  • Keep credentials in Cypress environment configuration or CI secrets, never in the spec source.

Cypress cy.request for API testing gives an SDET a fast way to call an HTTP endpoint directly from a Cypress test. Use it to verify API behavior, authenticate without a slow login screen, seed test data, and clean up records while keeping the browser focused on the user journey.

The command supports common HTTP methods, JSON and form bodies, headers, query strings, redirects, Basic Authentication, and expected error responses. This guide shows production-minded TypeScript patterns, explains the network model, and draws a clear line between a useful API check and a brittle test that happens to return 200.

TL;DR

Need Recommended pattern Key assertion
Read a resource cy.request('GET', '/api/users/42') Status plus resource identity
Create data cy.request('POST', '/api/users', body) 201 plus returned ID
Test validation Options object with failOnStatusCode: false Exact 4xx and error contract
Authenticate POST credentials, then preserve cookie or token A protected endpoint returns 200
Check redirect followRedirect: false Status, location, or redirectedToUrl
Prepare a UI test API seed in beforeEach or a helper UI behavior, not seed implementation

The response yields status, body, headers, and duration. A JSON response is parsed when its response content type ends in json. Assertions chained to cy.request() run once, so do not treat the command like a retried DOM query.

1. What Cypress cy.request for API Testing Does

cy.request() makes an HTTP request and yields the completed response. It must be chained from cy, and the server must send a response. The supported call shapes are concise: URL only, URL with body, method with URL, method with URL and body, or one options object. GET is the default method.

Unlike a click that causes the application to call its backend, cy.request() does not need a rendered page. That makes it useful at three layers. At the API layer, it checks endpoints and contracts. At the setup layer, it creates users, orders, or feature state before a UI journey. At the authentication layer, it exchanges credentials for a session without repeating login form steps in every test.

It is still a Cypress command, not a replacement for a general load test or an API client suite with service virtualization. It participates in the Cypress command queue, reads Cypress configuration, displays in the Command Log by default, and can share cookies with the browser context. It is ideal when API and browser checks belong to one end-to-end workflow.

A useful mental model is: arrange through the fastest trusted interface, act through the interface under test, and assert the business outcome. For example, create a project by API, edit it through the UI, and verify the persisted result through the API. This approach keeps setup short without removing meaningful user coverage. If you are designing the broader pyramid, the Cypress end-to-end testing guide provides complementary test-boundary guidance.

2. Configure TypeScript and the Request Base URL

A relative request URL needs a host. Before any page visit, Cypress resolves it against e2e.baseUrl. After a visit, Cypress can infer the visited host, but relying on a configured base URL makes intent clearer and reduces order-dependent behavior.

// cypress.config.ts
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    responseTimeout: 30_000,
  },
})

Then a smoke check can use a relative route.

describe('health API', () => {
  it('reports a ready service', () => {
    cy.request('/api/health').then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body).to.deep.include({ status: 'ready' })
      expect(response.headers).to.have.property('content-type')
    })
  })
})

If the frontend and API use different origins, keep the API origin in configuration and build a fully qualified URL. In current Cypress projects, cy.env() can retrieve environment values inside the command queue. An established repository may also read Cypress.env(), but new helpers should follow the configuration conventions used by that project. Never commit real credentials. Pass them through CI secrets or local ignored configuration.

Use explicit response types when they make assertions safer, but do not confuse TypeScript types with runtime validation. An interface can improve editor support while a server still returns malformed data. Runtime assertions remain necessary.

type HealthResponse = {
  status: 'ready' | 'degraded'
  version: string
}

cy.request<HealthResponse>('/api/health').its('body').then((body) => {
  expect(body.status).to.eq('ready')
  expect(body.version).to.be.a('string').and.not.be.empty
})

3. Use GET Requests and Query Parameters Correctly

For a simple read, cy.request('/api/items') is enough because GET is the default. Prefer the options object when the endpoint accepts filters or pagination. The qs option appends query parameters and lets Cypress handle encoding. This is clearer than concatenating user-controlled values into a URL.

type User = {
  id: string
  email: string
  active: boolean
}

it('filters active users by email', () => {
  cy.request<{ items: User[]; total: number }>({
    method: 'GET',
    url: '/api/users',
    qs: {
      status: 'active',
      email: 'qa+search@example.test',
      page: 1,
    },
  }).then(({ status, body, headers }) => {
    expect(status).to.eq(200)
    expect(headers['content-type']).to.include('application/json')
    expect(body.total).to.be.at.least(0)
    expect(body.items).to.be.an('array')
    body.items.forEach((user) => {
      expect(user.active).to.eq(true)
      expect(user.email).to.eq('qa+search@example.test')
    })
  })
})

An empty result can be a valid response. Assert the business rule, not a convenient fixture accident. If the test creates the target user first, it can require exactly one matching item. If it uses a shared environment, an assertion such as length.greaterThan(0) may be nondeterministic. Controlled data gives you stronger assertions.

Test pagination boundaries separately: the first page, a middle page with known data, an empty page beyond the result set, invalid negative values, and the maximum allowed size. Also assert stable fields such as resource IDs, ordering keys, or a documented total. Avoid asserting an entire response object when timestamps, trace IDs, or optional fields legitimately change.

Headers deserve focused checks. Content type, cache policy, rate-limit metadata, and correlation IDs can be part of the API contract. Header names in the yielded object are typically lowercase, so inspect the response once and assert the normalized key used by your environment.

4. Send POST, PUT, PATCH, and DELETE Requests

When a JavaScript object is passed as body, Cypress serializes it as JSON and sets the JSON content type. A creation test should verify more than the echoed input. Check the creation status, generated identifier, normalized data, and a follow-up read when persistence matters.

type Project = {
  id: string
  name: string
  ownerId: string
  status: 'draft' | 'active'
}

it('creates, updates, and deletes a project', () => {
  cy.request<Project>('POST', '/api/projects', {
    name: 'Release readiness',
    ownerId: 'user-42',
  }).then((createResponse) => {
    expect(createResponse.status).to.eq(201)
    expect(createResponse.body.id).to.be.a('string').and.not.be.empty
    expect(createResponse.body).to.include({
      name: 'Release readiness',
      ownerId: 'user-42',
    })

    const projectId = createResponse.body.id

    cy.request<Project>('PATCH', `/api/projects/${projectId}`, {
      status: 'active',
    }).then((updateResponse) => {
      expect(updateResponse.status).to.eq(200)
      expect(updateResponse.body.status).to.eq('active')
    })

    cy.request('DELETE', `/api/projects/${projectId}`).then((deleteResponse) => {
      expect([200, 204]).to.include(deleteResponse.status)
    })

    cy.request({
      url: `/api/projects/${projectId}`,
      failOnStatusCode: false,
    }).its('status').should('eq', 404)
  })
})

Use the status code your service actually documents. Do not accept both 200 and 204 merely because the API is inconsistent. The inclusive assertion above is appropriate only when the public contract intentionally allows either. A mature team should tighten that contract.

PUT normally represents full replacement and PATCH partial change, but implementation contracts vary. Test omitted fields deliberately. A high-value update test verifies whether an omitted property is preserved, defaulted, or removed. A delete test should also cover authorization, repeated deletion, and references that prevent deletion.

Nested Cypress commands execute reliably because they are queued inside .then(), but excessive nesting hurts readability. For larger workflows, return the chain from a helper or store the created ID with an alias. Keep lifecycle ownership obvious so failed tests do not leak data.

5. Handle Authentication, Cookies, Tokens, and Forms

cy.request() can perform Basic Authentication through the auth option. For bearer tokens, add an Authorization header. For form login endpoints, set form: true, which URL-encodes the body and applies the appropriate content type.

cy.request({
  method: 'POST',
  url: '/login',
  form: true,
  body: {
    username: 'qa.user',
    password: 'secret-from-test-config',
  },
}).its('status').should('eq', 200)

Cypress automatically sends relevant browser cookies with a request and applies response cookies back to the browser cookie store. That makes a direct login request a practical setup for a UI test. Verify it with a protected endpoint or a cookie assertion before visiting the application. For repeated authenticated tests, pair the login helper with Cypress cy.session patterns so the validated browser state can be restored instead of rebuilt.

A token-based API often returns JSON. Store only what the application itself expects. If the app reads localStorage, populate the same key and format, then validate against a protected route.

function loginByApi(email: string, password: string) {
  cy.request<{ accessToken: string }>({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
    log: false,
  }).then(({ status, body }) => {
    expect(status).to.eq(200)
    cy.window().then((win) => {
      win.localStorage.setItem('access_token', body.accessToken)
    })
  })
}

Masking the command with log: false reduces exposure in the Command Log, but it is not a complete secret-management system. Do not print the response, embed passwords in aliases, or include secrets in screenshots and artifacts. Also test missing, expired, malformed, and insufficient-scope credentials. Authentication success is only one branch of the contract.

6. Assert Responses as Business Contracts

A response assertion should answer four questions: did the protocol behave correctly, is the payload structurally usable, does the business rule hold, and did the operation persist when persistence is promised? Status-only checks miss most API defects.

Layer Useful assertions Example defect caught
HTTP Status, content type, location, cache headers Wrong redirect or media type
Structure Required keys, types, array shape Renamed or missing property
Business State transition, ownership, totals, invariants Unauthorized record or bad calculation
Persistence Follow-up GET or UI verification Success response without stored change
Observability Correlation header, safe error code Untraceable production failure

Write targeted Chai assertions that explain the contract. A deep equality assertion is valuable for a small, stable error object. It is noisy for a large resource with generated values.

cy.request<{ id: string; subtotal: number; tax: number; total: number }>(
  'POST',
  '/api/orders',
  { sku: 'BOOK-1', quantity: 2 },
).then(({ body, status }) => {
  expect(status).to.eq(201)
  expect(body.id).to.match(/^ord_/)
  expect(body.subtotal).to.be.greaterThan(0)
  expect(body.tax).to.be.at.least(0)
  expect(body.total).to.eq(body.subtotal + body.tax)
})

If your organization uses a JSON Schema validator, integrate a real maintained package through a custom command or task, and keep schema versions alongside the API contract. Do not invent a cy.validateSchema() method unless your project actually defines it. Plain assertions are portable and are often clearer for the few fields that drive a user outcome.

Remember that assertions attached to cy.request() execute once after the response. Cypress does not resend the request until the assertion passes. For eventually consistent systems, poll a read-only status endpoint with an explicit bounded strategy or use a backend-specific task. Never repeat a non-idempotent POST casually.

7. Test Errors, Redirects, and Retry Behavior

By default, Cypress fails a request whose response is outside the 2xx or 3xx ranges. That is a safe default for setup operations. When the purpose of the test is to inspect an error, set failOnStatusCode: false for that request and assert the exact failure contract.

it('rejects an invalid project name', () => {
  cy.request<{ code: string; field: string; message: string }>({
    method: 'POST',
    url: '/api/projects',
    body: { name: '', ownerId: 'user-42' },
    failOnStatusCode: false,
  }).then(({ status, body }) => {
    expect(status).to.eq(422)
    expect(body).to.deep.equal({
      code: 'VALIDATION_ERROR',
      field: 'name',
      message: 'Name is required',
    })
  })
})

Do not set failOnStatusCode: false in a global wrapper and forget to inspect the result. That converts server failures into passing setup. Negative tests should distinguish unauthenticated 401, forbidden 403, missing 404, conflict 409, rate limited 429, and validation 422 when those codes are part of the service contract.

To inspect a redirect rather than follow it, use followRedirect: false. The response can expose the redirect status, location header, and normalized redirectedToUrl behavior supported by Cypress. Assert the destination without navigating a browser.

Cypress retries transient network failures by default. retryOnStatusCodeFailure defaults to false and, when enabled, retries status failures up to the supported internal limit. Enable it only when retrying matches the endpoint semantics and your test goal. A retry can hide a reliability defect or repeat a state-changing request. Prefer fixing unstable readiness and deterministic data over applying retries broadly.

8. Create and Clean Up Test Data Safely

Direct API setup is one of the strongest reasons to use cy.request(). It avoids slow UI preparation and lets each test own a known state. The endpoint may be a public business API or a test-only seed route protected in non-production environments. Either way, make the operation deterministic.

type SeedResult = { userId: string; projectId: string }

function seedProject(testKey: string) {
  return cy.request<SeedResult>('POST', '/test-support/seed-project', {
    testKey,
    plan: 'pro',
    projectStatus: 'draft',
  }).then(({ status, body }) => {
    expect(status).to.eq(201)
    return body
  })
}

describe('project publishing', () => {
  it('publishes a valid draft', () => {
    const testKey = `publish-${Cypress._.random(1_000_000, 9_999_999)}`

    seedProject(testKey).then(({ projectId }) => {
      cy.visit(`/projects/${projectId}`)
      cy.contains('button', 'Publish').click()
      cy.contains('[role=alert]', 'Project published').should('be.visible')

      cy.request(`/api/projects/${projectId}`).its('body.status').should('eq', 'published')
    })
  })
})

Random suffixes reduce collisions, but an explicit test-run ID from CI gives better traceability. Cleanup can run through an idempotent endpoint keyed by that ID. Be careful with afterEach: Cypress may not execute cleanup after a process crash. A scheduled environment janitor or time-to-live policy should remove abandoned test data.

Avoid a single shared admin user whose mutable preferences are changed by every spec. Parallel jobs will race. Prefer isolated tenants, unique resources, or server-side resets with clear ownership. For more fixture design options, see reliable Cypress test data strategies.

9. Compare cy.request, cy.intercept, and Browser Actions

These tools operate at different boundaries. cy.request() originates outside the application browser request layer, so cy.intercept() does not capture it. This is a frequent source of confusion. Use intercept for requests made by the application, request for direct calls made by the test, and browser actions for visible user behavior.

Tool Starts the request Primary purpose Can change app network response Best assertion
cy.request() Test code Direct API call, setup, cleanup No app request is involved Response contract
cy.intercept() Application or browser Observe, wait for, or stub app traffic Yes Request and response caused by UI
cy.visit() and UI commands User simulation Exercise rendered workflow Indirectly causes app traffic Visible behavior and state

A balanced checkout test might create a product with cy.request(), register cy.intercept('POST', '/api/orders').as('createOrder'), submit checkout through the UI, wait for @createOrder, and finally read the created order with cy.request(). Each tool has one job.

Do not use direct requests to claim that buttons, forms, accessibility states, and client validation work. Conversely, do not drive ten screens just to create a precondition when a trusted setup API can do it in one call. Read cy.request versus cy.intercept for a deeper boundary comparison.

10. Build Reusable Helpers Without Hiding the Test

A helper should reduce repeated transport details while leaving business intent visible. Return the Cypress chain, accept meaningful inputs, assert setup success, and type the response. Avoid a universal wrapper with dozens of optional parameters because it recreates cy.request() badly and hides defaults.

// cypress/support/api.ts
export type CreateUserInput = {
  email: string
  role: 'member' | 'admin'
}

export type ApiUser = CreateUserInput & { id: string }

export function createUser(input: CreateUserInput) {
  return cy.request<ApiUser>({
    method: 'POST',
    url: '/api/users',
    body: input,
  }).then((response) => {
    expect(response.status).to.eq(201)
    return response.body
  })
}

A custom command is useful when fluent Cypress syntax improves common workflows, particularly authentication. Add the TypeScript declaration so consumers receive the correct subject type. A plain function is often simpler for domain factories and avoids expanding the global Chainable surface.

Log enough to diagnose failures, but protect secrets. Give created resources traceable names and include the endpoint and business key in assertion messages where helpful. In CI, separate backend unavailability from assertion failure through health checks and artifacts, not by swallowing errors.

Measure suite value rather than chasing the smallest duration number. Direct setup usually reduces runtime, but it can couple tests to internal endpoints. Treat test-support APIs as maintained contracts, secure them, and version changes with the suite.

11. Cypress cy.request for API Review Checklist

Before merging a new API-backed Cypress test, review it as an SDET would review production code.

  • The endpoint and method match the published service contract.
  • The test owns or deterministically locates its data.
  • Success assertions cover status, critical structure, and a business invariant.
  • Negative tests use failOnStatusCode: false locally and assert the exact error.
  • Secrets are supplied through configuration and omitted from logs.
  • A state-changing operation is not retried unless idempotency is guaranteed.
  • Setup failure cannot silently pass and contaminate the UI phase.
  • Cleanup is idempotent, with a fallback for interrupted runs.
  • cy.intercept() is used only for application-originated traffic.
  • Timeouts reflect a known service expectation instead of masking general slowness.

This checklist also improves interview answers. It shows that you understand API testing beyond syntax: isolation, observability, data ownership, retry safety, and the difference between protocol correctness and business correctness.

Interview Questions and Answers

Q: What does cy.request() yield?

It yields a response object with properties including status, body, headers, and duration. JSON bodies are parsed automatically when the response content type ends in json. I use .then() for related assertions or .its() for a focused property.

Q: Does cy.request() obey browser CORS restrictions?

It is not sent through the application browser request layer, so it is not constrained in the same way as browser-originated JavaScript. That is useful for setup, but it also means a passing direct request does not prove that the browser application is configured correctly for CORS.

Q: Why does cy.intercept() not see cy.request()?

cy.intercept() observes or controls traffic initiated by the application. cy.request() is initiated by Cypress outside that layer. I use cy.request() to call the service directly and intercept to inspect a request caused by a UI action.

Q: How do you test a 404 response?

I set failOnStatusCode: false on that specific request, then assert status is 404 and validate the stable error code or payload. I do not disable status failure globally because unexpected backend failures could then pass unnoticed.

Q: Are cy.request() assertions retried?

No. The request resolves and chained assertions run once. If the system is eventually consistent, I use an explicit, bounded polling design for a safe read endpoint instead of expecting Cypress query retry behavior.

Q: How would you use it for login?

I post credentials or a test identity to the authentication endpoint, allow the response cookie to populate the browser or store the returned token where the application expects it, and validate a protected endpoint. For repeated tests, I wrap that setup in cy.session() with a meaningful ID and validation callback.

Q: What is the biggest test-data risk?

Shared mutable state. Parallel specs can update or delete each other's records, causing false failures. I create unique, traceable data per test or worker and provide idempotent cleanup plus an out-of-band janitor for interrupted runs.

Common Mistakes

  • Asserting only 200, which misses incorrect payloads, authorization leaks, and failed persistence.
  • Disabling failOnStatusCode in a generic wrapper without requiring an expected status.
  • Expecting cy.intercept() to capture a direct cy.request().
  • Hard-coding tokens, passwords, hosts, or tenant IDs in committed specs.
  • Retrying non-idempotent POST requests and creating duplicate resources.
  • Using shared data whose state changes when specs run in parallel.
  • Treating a TypeScript interface as runtime response validation.
  • Driving the API for every step and then claiming full user-interface coverage.
  • Assuming a command assertion retries as a DOM query would.
  • Leaving created data behind with no ownership key or cleanup strategy.

Conclusion

Cypress cy.request for API testing is most valuable when it makes a test more direct without blurring the boundary under test. Configure a reliable base URL, use the options object for nontrivial requests, assert business contracts, and make authentication and data setup deterministic.

Start with one slow UI test that spends most of its time arranging data. Move only that arrangement to a typed API helper, preserve the meaningful browser action, and add a direct response or persistence assertion. That small refactor demonstrates the practical speed, clarity, and reliability benefits of cy.request().

Interview Questions and Answers

What is cy.request in Cypress?

cy.request is a Cypress command for making a direct HTTP request. It supports multiple call signatures and an options object for headers, query strings, authentication, forms, redirects, retries, and negative status handling. It yields a response with status, body, headers, and duration.

What is the difference between cy.request and cy.intercept?

cy.request initiates a direct call from the test. cy.intercept observes, waits for, or stubs requests initiated by the application. Because they operate at different network boundaries, cy.intercept does not capture cy.request calls.

How do you validate negative API cases with cy.request?

I set failOnStatusCode to false only for the expected negative request. Then I assert the precise HTTP status, stable error code, relevant field, and message contract. This prevents unrelated server errors from becoming false passes.

How do cookies work with cy.request?

Cypress sends relevant browser cookies with cy.request and applies cookies returned by the server to the browser cookie store. This lets a direct authentication request establish browser state. I still validate the session against a protected endpoint before using it.

How do you make cy.request tests reliable in parallel CI?

I create unique, traceable data per test or worker and avoid shared mutable accounts. Setup and cleanup endpoints are idempotent, and interrupted-run data has a TTL or janitor. I also avoid broad retries that can duplicate state-changing operations.

Are assertions on cy.request retried automatically?

No. The HTTP request completes and its assertions execute once. For eventual consistency, I use bounded polling against an idempotent read endpoint and a clear timeout, rather than assuming DOM-style query retries.

What should a strong API response assertion include?

It should cover the documented status, important headers, required structure, and at least one business invariant. For a state change, I also verify persistence through a follow-up read or meaningful UI outcome. A status-only assertion is rarely sufficient.

Frequently Asked Questions

Can Cypress cy.request be used for API testing?

Yes. cy.request can make direct HTTP calls and assert status, headers, response bodies, redirects, and duration. It is especially useful when API checks support an end-to-end browser workflow.

Does cy.request use the Cypress baseUrl?

A relative URL used before a page visit resolves against the configured e2e baseUrl. After a visit, Cypress can infer the visited host, but an explicit configuration or fully qualified API URL is clearer.

How do I test a failed API response in Cypress?

Set failOnStatusCode to false on the request that is expected to fail. Then assert the exact status and stable error contract so an unexpected failure cannot pass silently.

Can cy.request send authentication headers?

Yes. Use the auth option for HTTP Basic Authentication or the headers option for a bearer token or another documented scheme. Keep credentials in protected environment configuration and avoid command logging for sensitive exchanges.

Is cy.request captured by cy.intercept?

No. cy.request is initiated outside the application browser network layer, while cy.intercept handles application-originated traffic. Use each tool at its intended boundary.

Does cy.request retry failed status codes?

Not by default. retryOnStatusCodeFailure defaults to false, while retryOnNetworkFailure defaults to true. Enable status retries only when they match the endpoint semantics and do not risk repeating an unsafe state change.

Should API setup go in beforeEach?

It can, when every test needs fresh isolated state and setup failure should stop the test. A domain helper called inside the test is often clearer when only some scenarios need the data or when the created ID drives later steps.

Related Guides