Skip to content

API Testing with Cypress

Cypress isn't limited to browser UI testing, cy.request() lets you test an API directly, no UI involved. This lesson covers writing focused, fast API tests and combining them with UI tests for full coverage.

cy.request() for Direct HTTP Calls

cy.request() makes a real HTTP request directly, bypassing the browser's rendering entirely. Unlike cy.intercept(), which controls requests your application makes, cy.request() is the test itself acting as an HTTP client.

cy.request('GET', '/api/products').then((response) => {
  expect(response.status).to.equal(200);
  expect(response.body).to.have.length.greaterThan(0);
});

This test never opens a browser page, it validates the API contract directly and typically runs in milliseconds.

cy.request() Syntax

cy.request(url)
cy.request(method, url)
cy.request(method, url, body)
cy.request({
  method: 'POST',
  url: '/api/orders',
  body: { items: [...] },
  headers: { Authorization: 'Bearer token' },
})
  • The object form supports headers, body, query parameters, and more advanced options.
  • cy.request() automatically fails the test on a non-2xx/3xx response by default, unless failOnStatusCode: false is set.
  • Cookies from previous requests in the same test are sent automatically, useful for session-based auth.
  • cy.request() is not automatically retried, it runs exactly once per call.

API Testing Cheat Sheet

Common assertions and patterns for direct API tests.

Goal Example
Assert status code expect(response.status).to.equal(200)
Assert response shape expect(response.body).to.have.property('id')
Allow non-2xx without failing cy.request({ url, failOnStatusCode: false })
Send auth header cy.request({ headers: { Authorization: ... } })
Chain dependent requests Use .then() to use one response in the next request

Chaining Dependent API Requests

Real API test scenarios often need one request's result to feed into the next, creating a resource, then fetching or modifying it. .then() lets you build this chain while keeping each step's intent clear.

cy.request('POST', '/api/orders', { items: [{ id: 1, qty: 2 }] })
  .then((createResponse) => {
    expect(createResponse.status).to.equal(201);
    const orderId = createResponse.body.id;
    return cy.request('GET', `/api/orders/${orderId}`);
  })
  .then((getResponse) => {
    expect(getResponse.body.items).to.have.length(1);
  });

Combining API Tests with UI Tests

A strong testing strategy uses fast cy.request()-based tests to validate an API's contract thoroughly across many cases, while reserving full UI tests for confirming that the frontend correctly consumes and displays that data, avoiding redundant, slow coverage of the same logic at both levels.

  • Use API tests for exhaustive input/output validation of endpoints.
  • Use UI tests for confirming the frontend correctly renders and reacts to API data.
  • Avoid re-testing every API edge case again through the UI layer.

Common Mistakes

  • Testing every API edge case redundantly through slow, full UI-driven tests instead of direct cy.request() calls.
  • Forgetting cy.request() fails the test automatically on non-2xx responses unless explicitly configured otherwise.
  • Not realizing cy.request() shares cookies with the browser session, which can cause unexpected authenticated behavior.
  • Writing brittle chained requests without asserting on intermediate responses before using their data.

Key Takeaways

  • cy.request() makes real HTTP requests directly, without involving the browser UI.
  • It is ideal for fast, thorough validation of an API's contract and edge cases.
  • Dependent requests can be chained cleanly using .then().
  • A strong strategy pairs thorough API-level tests with focused UI tests, avoiding redundant coverage.

Pro Tip

Use cy.request()-based API tests to cover the long tail of input validation and error-response edge cases, then keep your UI tests focused on just a few representative happy-path and error scenarios, this combination catches more bugs with a much faster overall suite.