Why API Testing in Cypress? Combining UI and API Verification in One Tool

๐Ÿ“‹ Table of Contents โ–พ
  1. The Case for API Testing Inside Cypress
  2. Common Mistakes

Most QA teams use separate tools for API testing (Postman, REST Assured) and UI testing (Cypress, Selenium). This split creates blind spots: your Postman tests verify the API returns correct data, and your Cypress tests verify the UI displays data correctly โ€” but nobody verifies that the UI correctly interprets what the API returns. Cypress bridges this gap with cy.request(), letting you send HTTP requests directly from your test suite. You can validate API responses, use API calls to set up test state, and combine API and UI assertions in a single test.

The Case for API Testing Inside Cypress

API testing in Cypress is not a replacement for dedicated API test suites โ€” it is a complement that enables three powerful patterns: faster test setup, API-level validation alongside UI checks, and end-to-end contract verification.

// Three reasons to test APIs in Cypress

// โ”€โ”€ REASON 1: Faster test setup โ”€โ”€
// Instead of clicking through 5 pages to create an order:
// cy.visit('/shop'); cy.addToCart(0); cy.get('.checkout').click(); ...

// Use one API call:
cy.request('POST', '/api/orders', {
  items: [{ productId: 1, quantity: 2 }],
  shippingAddress: { city: 'London', postcode: 'SW1A 1AA' },
}).then((response) => {
  expect(response.status).to.eq(201);
  const orderId = response.body.id;
  // Now verify the UI displays this order correctly
  cy.visit(`/orders/${orderId}`);
  cy.get('[data-cy="order-status"]').should('contain.text', 'Pending');
});


// โ”€โ”€ REASON 2: API-level validation โ”€โ”€
// Verify the API contract independently of the UI

cy.request('GET', '/api/products').then((response) => {
  expect(response.status).to.eq(200);
  expect(response.body).to.be.an('array').with.length.greaterThan(0);
  expect(response.body[0]).to.have.all.keys(
    'id', 'name', 'price', 'description', 'imageUrl', 'inStock'
  );
  expect(response.body[0].price).to.be.a('number').and.be.greaterThan(0);
});


// โ”€โ”€ REASON 3: Combined API + UI assertion โ”€โ”€
// Verify the UI accurately reflects the API data

cy.request('GET', '/api/products').then((apiResponse) => {
  const apiProducts = apiResponse.body;

  cy.visit('/shop');

  // Verify the UI shows the same number of products as the API
  cy.get('.product-card').should('have.length', apiProducts.length);

  // Verify the first product's name matches API data
  cy.get('.product-card').first()
    .find('.product-name')
    .should('have.text', apiProducts[0].name);

  // Verify the first product's price matches API data
  cy.get('.product-card').first()
    .find('.product-price')
    .should('contain.text', `$${apiProducts[0].price.toFixed(2)}`);
});


// โ”€โ”€ cy.request vs cy.intercept โ€” different tools, different purposes โ”€โ”€

const COMPARISON = {
  'cy.request()': {
    what: 'Sends a REAL HTTP request from the Cypress Node.js server',
    when: 'API validation, test setup, data seeding, authentication',
    scope: 'Independent of the browser โ€” works without cy.visit()',
    returns: 'Full response: status, headers, body, duration',
  },
  'cy.intercept()': {
    what: 'Intercepts requests made BY THE BROWSER (the app under test)',
    when: 'Stubbing responses, waiting for app requests, modifying payloads',
    scope: 'Only catches requests from the browser โ€” not from cy.request()',
    returns: 'Interception object with request and response details',
  },
};
Note: cy.request() sends HTTP requests from the Cypress Node.js backend process, NOT from the browser. This means requests bypass the browser’s CORS restrictions, cookie policies, and security headers. This is a feature, not a bug โ€” it lets you call any API endpoint regardless of cross-origin restrictions, making it ideal for test setup (calling admin APIs your application’s frontend cannot access) and backend validation (checking database state via internal APIs).
Tip: The combined API + UI assertion pattern is the highest-value pattern in this chapter. By fetching data from the API and then verifying the UI displays it correctly, you catch an entire category of defects: data transformation bugs, formatting errors, missing fields, and rendering issues. The API response is the “ground truth,” and the UI must match it. If the API returns a price of 29.99 but the UI shows 2999, this combined test catches it instantly.
Warning: cy.request() does not render the response in the browser โ€” it operates entirely at the network level. If you need to verify what the UI does with an API response (loading states, error handling, conditional rendering), use cy.intercept() to stub or spy on the browser’s request. Use cy.request() for setup and backend validation; use cy.intercept() for front-end behaviour testing.

Common Mistakes

Mistake 1 โ€” Using cy.visit() + UI clicks for test setup when cy.request() is faster

โŒ Wrong: Creating a test order by navigating through 5 pages of the checkout flow in beforeEach() โ€” adds 10 seconds per test.

โœ… Correct: cy.request('POST', '/api/orders', orderData) โ€” creates the order in milliseconds, then cy.visit('/orders/id') to verify the UI displays it.

Mistake 2 โ€” Confusing cy.request() with cy.intercept()

โŒ Wrong: Using cy.request() to “intercept” the application’s API calls โ€” it cannot do this; it sends its own requests.

โœ… Correct: Using cy.request() for sending test requests and cy.intercept() for monitoring/stubbing the application’s requests.

🧠 Test Yourself

What is the primary advantage of combining cy.request() API validation with cy.get() UI assertions in a single test?