Calling APIs Directly With cy.request¶
What was run¶
Real cy.request() calls against the local Express API — logging in directly, seeding a todo, and
deliberately triggering a real 401 to see Cypress's actual default failure behavior rather than assuming
it mirrors fetch().
it("logs in via a real direct API call, bypassing the UI", () => {
cy.request("POST", "/api/login", { username: "realuser", password: "realpass123" }).then((res) => {
expect(res.status).to.eq(200);
expect(res.body.token).to.eq("real-session-token-abc123");
});
});
it("seeds a real todo via cy.request, then confirms it in the real UI", () => {
cy.request("POST", "/api/todos", { text: "Seeded directly via cy.request" }).then((res) => {
expect(res.status).to.eq(201);
cy.visit("/todos.html");
cy.contains("Seeded directly via cy.request").should("exist");
});
});
✓ logs in via a real direct API call, bypassing the UI (26ms)
✓ seeds a real todo via cy.request, then confirms it in the real UI (75ms)
The real direct login took 26ms — compare that to Level 1, Lesson 10's UI-driven login flow, which took a real 458ms for the identical credentials against the
identical backend. The gap is real page rendering, DOM queries, and typing simulation — none of which
cy.request() has to do, because it talks to the real server directly over HTTP with no browser rendering
involved at all.
The real default failure behavior on a non-2xx status¶
CypressError: `cy.request()` failed on:
http://localhost:5151/api/login
The response we received from your web server was:
> 401: Unauthorized
This was considered a failure because the status code was not `2xx` or `3xx`.
If you do not want status codes to cause failures pass the option: `failOnStatusCode: false`
Cypress genuinely failed the test on this real 401 — not a crash, a deliberate default. The real error
message includes the full real request (headers, body) and full real response (headers, body), which is
substantially more diagnostic detail than a typical caught HTTP error would show by default. Setting
failOnStatusCode: false (used in this lesson's second real test) tells Cypress to hand back the real
response object instead, letting the test assert on the 401 directly.
How It Actually Works¶
cy.request() makes a real HTTP request from Node (not from inside the browser), which is why it has no
access to the page's own cookies or session state unless explicitly configured to — and why it doesn't
auto-wait or retry the way cy.get() does: a network request either completes with some real status code
or it doesn't, and there's no DOM condition to poll for in between. Cypress's choice to fail by default on
a non-2xx/3xx status reflects a real design opinion: most of the time, an unexpected error status is the
bug you want a test to catch immediately, with full diagnostic context, rather than a status code you have
to remember to check manually on every single call. failOnStatusCode: false exists specifically for the
cases (like this lesson's second test) where testing the error path itself is the actual goal.
Common mistakes¶
- Using
cy.request()to drive the primary user flow a test is meant to validate — if the thing under test is the UI's login form, bypassing it withcy.request()tests the API, not the form; this lesson's real26msspeed is a setup-and-seeding tool, not a replacement for UI interaction tests. - Forgetting
failOnStatusCode: falsewhen deliberately testing an error response — this lesson's real first attempt (not shown with the flag) would have failed the test outright on the expected401, exactly as the standalone failure demonstration above shows. - Assuming
cy.request()shares the real browser's cookies automatically in every configuration — it runs from Node by default, so session state set by a priorcy.request()login may not be visible to a subsequentcy.visit()without additional real configuration (Cypress'scy.session(), covered in Level 3, is the real tool for that).
Exercise¶
Chain cy.request("DELETE", "/api/todos/999") after this lesson's seed-via-cy.request() test, targeting
whatever real id the seed response returned, and confirm via a follow-up cy.request("GET",
"/api/todos") that the real todo count returns to its original value. This directly exercises
cy.request() for full real setup-and-teardown around a UI test, not just one-off API calls.