Resource library

QA How-To

Cypress cy.request for API: Examples

Copy practical cypress cy.request for API example tests covering CRUD, auth, query strings, errors, redirects, fixtures, polling, and UI setup in TypeScript.

20 min read | 2,191 words

TL;DR

The most flexible Cypress cy.request example uses an options object with method, url, body, headers, qs, and behavior flags. For maintainable tests, wrap only domain-specific setup, return the chain, and assert protocol plus business behavior in every example.

Key Takeaways

  • Start with the shortest cy.request signature, then switch to the options object as soon as the test needs explicit controls.
  • Type the response for editor support, but keep runtime assertions for every contract that matters.
  • Create scenario data through a helper that returns the generated ID instead of relying on shared fixtures.
  • Use local failOnStatusCode false only for negative cases, and assert the full stable error contract.
  • Treat redirects, binary downloads, authentication cookies, and eventual consistency as separate test patterns.
  • Combine direct API arrangement with browser actions and application network observation for focused end-to-end coverage.
  • Return Cypress chains from helpers so command order and failures stay visible to the runner.

A useful cypress cy.request for API example should be more than a one-line GET that checks for 200. The examples below show how working QA teams create records, authenticate, test invalid input, inspect redirects, verify downloads, poll safe endpoints, and connect API setup to a browser scenario.

Every snippet uses current Cypress command shapes and TypeScript. The app-specific examples assume a conventional local test API under /api, while the opening smoke spec uses a public demo endpoint so you can run it without building a backend first. Replace demo data and routes with contracts owned by your service.

TL;DR

cy.request<{ id: string; name: string }>({
  method: 'POST',
  url: '/api/projects',
  headers: { 'x-test-run': 'local-example' },
  body: { name: 'API example project' },
}).then(({ status, body }) => {
  expect(status).to.eq(201)
  expect(body.id).to.be.a('string').and.not.be.empty
  expect(body.name).to.eq('API example project')
})

Use the options-object form when the test needs query parameters, headers, authentication, redirects, encoding, retries, or expected error responses. Use failOnStatusCode: false only when the response itself is under test.

1. Cypress cy.request for API Example Project

Install Cypress in a TypeScript project, define an end-to-end base URL, and put API specs under cypress/e2e. The public test below is deliberately small and runnable. It proves command syntax, generic response typing, and meaningful assertions without depending on the application under development.

// cypress/e2e/api/jsonplaceholder.cy.ts
type DemoUser = {
  id: number
  name: string
  email: string
  address: { city: string }
}

describe('JSONPlaceholder API', () => {
  it('reads a known user', () => {
    cy.request<DemoUser>(
      'GET',
      'https://jsonplaceholder.typicode.com/users/1',
    ).then((response) => {
      expect(response.status).to.eq(200)
      expect(response.headers['content-type']).to.include('application/json')
      expect(response.body).to.include({ id: 1, name: 'Leanne Graham' })
      expect(response.body.email).to.match(/@/)
      expect(response.body.address.city).to.be.a('string').and.not.be.empty
    })
  })
})

Run it with npx cypress run --spec cypress/e2e/api/jsonplaceholder.cy.ts. A public demo service is suitable for learning, not as a dependency of a critical CI pipeline. Your production suite should test a controlled environment with known data, ownership, and availability.

For local application examples, configure the host once.

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

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

Relative calls such as cy.request('/api/health') now resolve against that base URL before a page is visited. If the API has a different host, build a fully qualified URL from protected Cypress configuration instead of scattering origins through specs.

2. GET Examples for Resources, Search, and Pagination

A single-resource check should assert identity and contract fields, not the full payload if it contains volatile timestamps or audit data.

type Project = {
  id: string
  name: string
  status: 'draft' | 'published'
  owner: { id: string; displayName: string }
}

it('returns one project by ID', () => {
  cy.request<Project>('/api/projects/prj_1001').then(({ status, body }) => {
    expect(status).to.eq(200)
    expect(body.id).to.eq('prj_1001')
    expect(body.name).to.be.a('string').and.not.be.empty
    expect(body.status).to.be.oneOf(['draft', 'published'])
    expect(body.owner.id).to.match(/^usr_/)
  })
})

For search and pagination, use qs. It handles query-string construction and keeps filters readable. Seed records first when the test needs an exact result count.

type ProjectPage = {
  items: Project[]
  page: number
  pageSize: number
  total: number
}

it('returns the second page in descending creation order', () => {
  cy.request<ProjectPage>({
    url: '/api/projects',
    qs: {
      ownerId: 'usr_2001',
      page: 2,
      pageSize: 10,
      sort: 'createdAt:desc',
    },
  }).then(({ status, body }) => {
    expect(status).to.eq(200)
    expect(body.page).to.eq(2)
    expect(body.pageSize).to.eq(10)
    expect(body.items.length).to.be.at.most(10)
    expect(body.total).to.be.at.least(body.items.length)
    body.items.forEach((project) => {
      expect(project.owner.id).to.eq('usr_2001')
    })
  })
})

Do not assert items.length is 10 unless the controlled dataset guarantees a full second page. Boundary examples should include an empty page past the end, an unsupported sort key, the largest allowed page size, and invalid values. Those cases reveal parsing and validation bugs that a happy-path GET misses.

3. CRUD Example With Setup and Cleanup

This example owns its record from creation through deletion. It returns IDs through Cypress chains and validates persistence after each state change. The afterEach cleanup pattern can be helpful, but a self-contained lifecycle is easier to follow for a tutorial.

type Task = {
  id: string
  title: string
  priority: 'low' | 'medium' | 'high'
  completed: boolean
}

describe('task lifecycle', () => {
  it('creates, reads, changes, and deletes a task', () => {
    cy.request<Task>('POST', '/api/tasks', {
      title: 'Review release candidate',
      priority: 'high',
    }).then((created) => {
      expect(created.status).to.eq(201)
      expect(created.body).to.include({
        title: 'Review release candidate',
        priority: 'high',
        completed: false,
      })

      const taskId = created.body.id

      cy.request<Task>(`/api/tasks/${taskId}`).then((read) => {
        expect(read.status).to.eq(200)
        expect(read.body.id).to.eq(taskId)
      })

      cy.request<Task>('PATCH', `/api/tasks/${taskId}`, {
        completed: true,
      }).then((updated) => {
        expect(updated.status).to.eq(200)
        expect(updated.body.completed).to.eq(true)
        expect(updated.body.title).to.eq('Review release candidate')
      })

      cy.request('DELETE', `/api/tasks/${taskId}`).its('status').should('eq', 204)

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

The PATCH assertion verifies that an omitted title remains unchanged. That is an important partial-update rule. If the service documents DELETE as idempotent, add a second deletion and assert its documented status. If deletion is blocked when references exist, create that reference and assert the conflict response separately.

Avoid accepting several possible status codes unless the contract explicitly allows them. Flexible assertions often preserve API inconsistency rather than make a test robust.

4. Authentication Examples for Cookies and Bearer Tokens

A session-cookie login can be established by a form or JSON request. When the server sets a cookie, Cypress synchronizes it with the browser cookie store. The test can confirm the cookie and call a protected resource.

function loginWithCookie(email: string, password: string) {
  return cy.request({
    method: 'POST',
    url: '/api/auth/session',
    body: { email, password },
    log: false,
  }).then((response) => {
    expect(response.status).to.eq(204)
    cy.getCookie('session_id').should('exist')
    cy.request('/api/me').its('status').should('eq', 200)
  })
}

it('loads the account with an API-created cookie session', () => {
  loginWithCookie('member@example.test', 'secret-from-config')
  cy.visit('/account')
  cy.contains('h1', 'Your account').should('be.visible')
})

For a bearer token, extract the token from the login body and use it in later direct calls. If the browser application stores the token in local storage, write the same key before visiting.

type TokenResponse = { accessToken: string; expiresIn: number }

cy.request<TokenResponse>({
  method: 'POST',
  url: '/api/auth/token',
  body: {
    clientId: 'cypress-e2e',
    clientSecret: 'secret-from-config',
  },
  log: false,
}).then(({ body }) => {
  expect(body.expiresIn).to.be.greaterThan(0)

  cy.request({
    url: '/api/admin/audit-events',
    headers: { Authorization: `Bearer ${body.accessToken}` },
    log: false,
  }).its('status').should('eq', 200)
})

Never include a password or token in a cy.session() ID, alias name, test title, or error message. For repeated logins, use Cypress cy.session examples with a validation request. log: false limits Command Log exposure, but CI secret hygiene still matters.

5. Negative Cypress cy.request for API Example Cases

Negative tests need failOnStatusCode: false because Cypress otherwise fails before your assertions inspect a non-2xx or non-3xx response. Keep the flag local and make the expected error specific.

type ApiError = {
  code: string
  message: string
  details?: Array<{ field: string; reason: string }>
}

it('rejects a task without a title', () => {
  cy.request<ApiError>({
    method: 'POST',
    url: '/api/tasks',
    body: { title: '', priority: 'high' },
    failOnStatusCode: false,
  }).then(({ status, body, headers }) => {
    expect(status).to.eq(422)
    expect(headers['content-type']).to.include('application/json')
    expect(body.code).to.eq('VALIDATION_ERROR')
    expect(body.details).to.deep.include({
      field: 'title',
      reason: 'required',
    })
  })
})

Build a small, intentional negative matrix.

Scenario Expected status Important assertion
No credentials 401 Stable auth error code
Valid identity, wrong role 403 No protected data in body
Unknown ID 404 Requested resource type or ID
Duplicate unique value 409 Conflict field and safe message
Invalid field 422 Field-level validation detail
Request over limit 429 Documented retry metadata

Do not merely check that a status is above 399. The difference between 401 and 403 is security behavior. Also verify that errors do not expose stack traces, SQL text, internal paths, credentials, or personal data. A concise public error contract and a correlation ID are far more useful than a raw exception.

6. Redirect, Header, Form, and Binary Examples

To test a redirect without following it, set followRedirect: false. Then assert the redirect status and destination.

it('redirects a signed-out account request to login', () => {
  cy.request({
    url: '/account',
    followRedirect: false,
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.eq(302)
    expect(response.headers.location).to.eq('/login')
  })
})

For a URL-encoded form endpoint, use form: true rather than manually serializing fields.

cy.request({
  method: 'POST',
  url: '/oauth/token',
  form: true,
  body: {
    grant_type: 'client_credentials',
    client_id: 'cypress-client',
    client_secret: 'secret-from-config',
  },
  log: false,
}).its('status').should('eq', 200)

A download can be requested with binary encoding and written as binary. This checks transport and file signature without a browser download dialog.

it('downloads a PDF invoice', () => {
  cy.request({
    url: '/api/invoices/inv_1001.pdf',
    encoding: 'binary',
  }).then((response) => {
    expect(response.status).to.eq(200)
    expect(response.headers['content-type']).to.include('application/pdf')
    expect(response.body.slice(0, 4)).to.eq('%PDF')
    cy.writeFile('cypress/downloads/inv_1001.pdf', response.body, 'binary')
  })
})

Transport checks do not prove the PDF text and layout are correct. For document content, parse it through a controlled Node task or specialized test utility. Keep the direct request example focused on status, media type, and a minimal file signature.

7. Data-Driven Examples With Fixtures

A fixture is useful for readable input cases, but generated resource IDs and timestamps should not be frozen in a shared file. Store stable scenarios in the fixture and build unique fields at runtime.

[
  { "name": "minimum valid task", "priority": "low", "expectedStatus": 201 },
  { "name": "normal task", "priority": "medium", "expectedStatus": 201 },
  { "name": "urgent task", "priority": "high", "expectedStatus": 201 }
]
type TaskCase = {
  name: string
  priority: 'low' | 'medium' | 'high'
  expectedStatus: number
}

describe('task priorities', () => {
  let cases: TaskCase[]

  before(() => {
    cy.fixture<TaskCase[]>('task-cases').then((data) => {
      cases = data
    })
  })

  it('accepts each supported priority', () => {
    cy.wrap(cases).each((testCase) => {
      const title = `${testCase.name}-${Cypress._.random(100000, 999999)}`

      cy.request('POST', '/api/tasks', {
        title,
        priority: testCase.priority,
      }).then((response) => {
        expect(response.status).to.eq(testCase.expectedStatus)
        expect(response.body).to.include({ title, priority: testCase.priority })
      })
    })
  })
})

One test with many cases stops at the first failure and can make reporting less precise. Separate generated tests are better when each row deserves independent status, but define them synchronously when the spec loads. Do not create Mocha tests inside a Cypress command callback. Another option is to keep a small set of high-value cases as explicit it blocks.

Use data-driven testing to clarify coverage, not to generate hundreds of near-identical HTTP calls. Pairwise selection and boundary analysis usually provide more signal than exhaustive combinations.

8. Poll an Eventually Consistent Read Safely

cy.request() assertions do not retry like Cypress DOM queries. If a background job moves through queued, running, and completed, use an explicit bounded polling helper. Poll only an idempotent read endpoint and fail with useful context.

type Job = {
  id: string
  status: 'queued' | 'running' | 'completed' | 'failed'
  resultUrl?: string
}

function waitForJob(jobId: string, attemptsLeft = 10): Cypress.Chainable<Job> {
  return cy.request<Job>(`/api/jobs/${jobId}`).then(({ body }) => {
    if (body.status === 'completed') return body
    if (body.status === 'failed') {
      throw new Error(`Job ${jobId} failed`)
    }
    if (attemptsLeft === 0) {
      throw new Error(`Job ${jobId} did not complete`)
    }

    return cy.wait(500).then(() => waitForJob(jobId, attemptsLeft - 1))
  })
}

it('exports an account report', () => {
  cy.request<{ jobId: string }>('POST', '/api/exports', {
    report: 'account-summary',
  }).then(({ body }) => waitForJob(body.jobId))
    .then((job) => {
      expect(job.resultUrl).to.match(/^/api/downloads//)
    })
})

The limits above are illustrative. Choose an interval and deadline from the system's service expectation. For long-running jobs, a backend task or event-based test may be better than keeping a browser runner occupied. Never recursively repeat the POST that starts the job.

For more synchronization guidance, see Cypress wait and retry patterns.

9. Combine API Setup With a UI Assertion

The strongest end-to-end use of cy.request() is often setup, not the primary action. Create the exact state through an API, then exercise the behavior users depend on. Use cy.intercept() for the application request caused by the browser.

type CartSeed = { cartId: string; itemName: string }

it('removes an item from the cart', () => {
  cy.request<CartSeed>('POST', '/test-support/carts', {
    user: 'member-1',
    items: [{ sku: 'KEYBOARD-1', quantity: 1 }],
  }).then(({ body }) => {
    cy.intercept('DELETE', `/api/carts/${body.cartId}/items/*`).as('removeItem')

    cy.visit(`/cart/${body.cartId}`)
    cy.contains('[data-testid=cart-item]', body.itemName)
      .find('button')
      .contains('Remove')
      .click()

    cy.wait('@removeItem').its('response.statusCode').should('eq', 204)
    cy.contains('[data-testid=cart-item]', body.itemName).should('not.exist')

    cy.request(`/api/carts/${body.cartId}`).its('body.items').should('have.length', 0)
  })
})

The test has four clear boundaries: API arrangement, visible UI action, app-network observation, and persisted-state verification. cy.intercept() cannot observe the direct setup or final request because those calls originate from the test. The cy.request and cy.intercept comparison explains that distinction in depth.

Use this hybrid pattern selectively. Verifying every UI state again through the backend can increase coupling. Add a final API read when persistence, distributed processing, or hidden state is the risk you want to cover.

10. Cypress cy.request for API Example Suite Design

Move repeated domain operations into plain functions that return a chain. Keep assertions for setup success inside the helper, and keep scenario-specific expectations in the spec.

// cypress/support/api/tasks.ts
export type NewTask = { title: string; priority: 'low' | 'medium' | 'high' }
export type SavedTask = NewTask & { id: string; completed: boolean }

export function createTask(input: NewTask): Cypress.Chainable<SavedTask> {
  return cy.request<SavedTask>('POST', '/api/tasks', input).then((response) => {
    expect(response.status, 'task setup status').to.eq(201)
    return response.body
  })
}

export function deleteTask(id: string): Cypress.Chainable<void> {
  return cy.request({
    method: 'DELETE',
    url: `/api/tasks/${id}`,
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status, 'cleanup status').to.be.oneOf([204, 404])
  })
}

A 404 is acceptable in this cleanup helper only because deletion is intentionally idempotent. The create helper accepts only business inputs, not arbitrary request options. That preserves the meaning of createTask() and prevents tests from bypassing its contract.

Organize specs by capability or resource, not merely by HTTP method. A tasks-api.cy.ts lifecycle is easier to reason about than one global post-tests.cy.ts. Tag or group pure API checks separately if the CI pipeline needs fast service feedback before browser tests.

Before copying any example, confirm the API's exact status codes, field names, authentication flow, and data ownership. Code that runs against the wrong contract is not reusable. Code that makes assumptions explicit is.

11. Review the Example Before Committing It

A copied request becomes trustworthy only after a contract review. Confirm whether the endpoint accepts JSON or form data, whether authentication is cookie-based or token-based, and whether its documented success code is 200, 201, 202, or 204. Check which fields are generated, nullable, normalized, or eventually updated. Replace tutorial credentials and fixed IDs with controlled configuration and owned test data.

Run the spec repeatedly and in a shuffled or parallel group. If the outcome changes, inspect shared accounts, uniqueness constraints, cleanup, and assumptions about execution order. A passing isolated run can still hide a collision that appears when two CI workers create the same email. Include a test-run key in created records when the service supports it, and make cleanup safe to repeat.

Review failure diagnostics as well as success. A useful assertion identifies the operation and business key. A helper that reports only expected 201 but got 500 forces the engineer to search for context. Preserve the Cypress Command Log for ordinary requests, hide only sensitive exchanges, and ensure server correlation IDs are accessible in CI artifacts when failures require backend tracing.

Finally, decide whether the example belongs in an API-focused spec or a browser journey. Pure contract checks can run early and quickly. Hybrid tests should keep direct setup short, exercise one meaningful UI behavior, and avoid duplicating every backend assertion. This review turns a runnable snippet into a maintainable test.

Interview Questions and Answers

Q: Which cy.request() syntax do you prefer?

I use a short positional form for a plain GET or simple JSON POST. I choose the options object when the request needs headers, query parameters, form encoding, redirect handling, retry flags, custom encoding, or failOnStatusCode. The options object scales without hiding important behavior.

Q: How do you pass query parameters?

I use the qs property in the options object. Cypress appends and encodes those values, which is clearer and safer than string concatenation. I then assert both page metadata and filter invariants in the returned items.

Q: How do you share a created ID with later commands?

I keep dependent commands in a .then() callback or return the ID from a domain helper and continue the Cypress chain. I avoid assigning a value to a normal variable and reading it before the queued request has executed.

Q: How do you test eventual consistency?

I poll a safe read endpoint with a bounded attempt count or deadline, fail immediately on a terminal error state, and provide a diagnostic message on timeout. I never repeat the non-idempotent operation that started the process.

Q: Is a public demo API suitable for CI?

It is useful for learning syntax, but it is a poor critical dependency. Availability, rate limits, data, and contract changes are outside the team's control. A reliable suite uses an owned environment or a controlled service double at the correct test layer.

Q: How do you test a file download through cy.request()?

I choose a supported encoding such as binary, assert the status and content type, inspect a minimal file signature, and optionally write the body with the same encoding. Content and layout validation should use a suitable parser rather than relying only on transport checks.

Q: What makes an API helper maintainable?

It has a domain name, typed input and output, returns the Cypress chain, and asserts setup success. It does not expose every cy.request() option or hide scenario-specific assertions. This keeps tests readable and failure locations useful.

Common Mistakes

  • Copying an endpoint and status from an example without matching the real service contract.
  • Reading a value from a normal variable before the Cypress command queue has assigned it.
  • Putting failOnStatusCode: false in every request wrapper.
  • Calling cy.intercept() and expecting it to capture a test-originated request.
  • Using a shared fixture record that parallel jobs mutate.
  • Retrying the POST that starts an asynchronous job.
  • Verifying a PDF only by checking that some bytes were returned.
  • Logging access tokens or embedding secrets in session IDs and aliases.
  • Building one generic API wrapper that hides methods, defaults, and failures.
  • Generating Mocha it blocks from data loaded asynchronously inside Cypress commands.

Conclusion

The right cypress cy.request for API example is the one that expresses a real contract and owns its data. Start with a direct request, add typed response handling, then assert protocol details, critical structure, and a business rule. Use focused patterns for authentication, negative responses, redirects, downloads, and eventual consistency rather than forcing every case through one wrapper.

Copy the closest recipe into a controlled test environment, replace the route and expected contract, and run it alone before integrating it with the UI. Once it is deterministic, extract only repeated domain setup into a helper and leave the scenario readable in the spec.

Interview Questions and Answers

Show the available cy.request call styles.

Cypress supports a URL, URL plus body, method plus URL, method plus URL plus body, and an options object. GET is the default. I prefer the options object for any request with headers, query strings, auth, encoding, redirects, or expected failures.

How do you write a CRUD test with cy.request?

I create a unique record and assert the creation contract, read it by its returned ID, update one field and verify preserved fields, delete it, then confirm the documented missing-resource behavior. The test owns its data and cleanup is idempotent.

How do you prevent false passes in negative API tests?

I scope failOnStatusCode false to one expected negative call and assert the precise status, stable error code, and relevant details. A broad assertion such as status greater than 399 is insufficient because it accepts unrelated server failures.

How do you handle asynchronous backend processing?

I poll an idempotent status endpoint with a clear limit, return on the expected terminal state, and fail early on a terminal error. I do not retry the command that creates the job because that can duplicate work.

Why return a Cypress chain from an API helper?

Returning the chain preserves Cypress command ordering, propagates failures, and lets the caller continue with the yielded domain object. It also avoids unreliable synchronization through external mutable variables.

How do you combine cy.request with a browser test?

I arrange deterministic state with cy.request, perform the meaningful action through the browser, use cy.intercept for the application request when useful, and optionally verify persisted state with a final direct GET. Each tool covers a distinct boundary.

What should be typed in a TypeScript API test?

I type stable request and response shapes to improve editor feedback and helper contracts. I still make runtime assertions because TypeScript does not validate server data. Generated or volatile fields get focused type and format assertions rather than full deep equality.

Frequently Asked Questions

What is a simple Cypress cy.request GET example?

Use cy.request('/api/resource').then((response) => { ... }) and assert the documented status plus important body fields. GET is the default method, so an explicit method is optional for the simplest call.

How do I send a POST body with cy.request?

Call cy.request('POST', '/api/resource', body) or use the options object with method, url, and body. When body is a JavaScript object, Cypress serializes it as JSON and applies the JSON content type.

How do I add query parameters to cy.request?

Use the qs property in the options object. Cypress appends and encodes the query parameters, which keeps filters and pagination values readable.

How can I reuse a response value in Cypress?

Continue inside the request's then callback, return a value from that callback, or return a domain helper's Chainable. Do not expect a normal variable assignment to run synchronously before later queued commands.

Can cy.request download a binary file?

Yes. Set a supported encoding such as binary, assert the response headers and file signature, and pass the same encoding to cy.writeFile if the test needs a local artifact. Use a parser for deeper file-content validation.

How do I test 401 or 422 responses?

Set failOnStatusCode to false for that request, then assert the exact status and safe, stable error fields. Keep the option local so unexpected failures in setup requests still stop the test.

Should I use cy.request or cy.intercept in a UI test?

Use cy.request for direct setup, cleanup, or backend verification. Use cy.intercept to observe or stub a request made by the application after a browser action. Many focused end-to-end tests use both at different boundaries.

Related Guides