Waiting for API Calls with Aliases

8 min read

Aliases are the syntax-level glue between cy.intercept and the rest of the test. By naming an intercepted request with .as("name"), you can pause the test on it (cy.wait("@name")), assert against it (cy.get("@name")), and chain it cleanly into multi-step flows. This lesson focuses on every aliased-wait pattern you'll need: single waits, multiple waits, repeated waits for paginated endpoints, and timeout tuning for slow services.

Aliasing an intercept

The pattern is exactly what you've seen in the previous two lessons — register an intercept and immediately call .as():

cy.intercept("GET", "/api/users").as("getUsers");

@getUsers is now the name you use everywhere else. Conventions:

  • Match the alias to the verb-and-noun: getUsers, createOrder, deletePost. Do not name them after route paths or HTTP methods alone.
  • Keep them descriptive enough that a cy.wait("@createOrder") reads as English.
  • One alias per intercept — re-using a name overwrites the previous registration.

cy.wait("@alias") — the canonical pause

The most common use of an alias is cy.wait — pause the test until that specific request fires and resolves:

cy.intercept("GET", "/api/users").as("getUsers");
 
cy.visit("/users");
cy.wait("@getUsers");
 
cy.get("[data-testid='user-row']").should("have.length", 10);

cy.wait("@getUsers") is condition-based. The test continues the moment the request returns — usually under a hundred milliseconds. It also yields the captured interception object, so you can chain .then to inspect:

cy.wait("@getUsers").then((interception) => {
  expect(interception.response?.statusCode).to.equal(200);
  expect(interception.response?.body.users).to.have.length(10);
});

This is the pattern that replaces every cy.wait(2000) in your codebase. Slow tests get faster; fast tests stop being flaky; failures point straight at the request that didn't fire.

Why aliases beat fixed waits

Three reasons, every time:

  • Reliable. The wait completes when the actual request resolves — not when an arbitrary timer ends. CI machines under load and dev machines under no load both produce stable behaviour.
  • Fast. A real request that takes 50ms unblocks the test in 50ms. cy.wait(2000) always takes 2000ms.
  • Diagnosable. If the request never fires, cy.wait("@alias") fails with "no matching request was made," pointing you at exactly what's wrong. cy.wait(2000) succeeds blindly and the next assertion fails for a confusing reason.

If a test has any cy.wait(<number>) calls left in it after this lesson, treat them as code smells in review.

Waiting on multiple requests at once

When a page boots and fires three independent requests simultaneously, wait on all of them in one call:

cy.intercept("GET", "/api/users/me").as("getMe");
cy.intercept("GET", "/api/notifications").as("getNotifications");
cy.intercept("GET", "/api/products").as("getProducts");
 
cy.visit("/dashboard");
cy.wait(["@getMe", "@getNotifications", "@getProducts"]);
 
cy.get("[data-testid='dashboard-loaded']").should("be.visible");

cy.wait with an array yields an array of interceptions in the same order. So you can still inspect each:

cy.wait(["@getMe", "@getProducts"]).then((interceptions) => {
  expect(interceptions[0].response?.body.role).to.equal("admin");
  expect(interceptions[1].response?.body).to.have.length.greaterThan(0);
});

This is the right shape for any page that does parallel fetches on mount.

Waiting on the same alias multiple times

When a single endpoint fires repeatedly (pagination, infinite scroll, polling), cy.wait("@alias") consumes one occurrence each time. Call it again to wait for the next:

cy.intercept("GET", "/api/products*").as("getProducts");
 
cy.visit("/products");
cy.wait("@getProducts");        // initial page-1 fetch
 
cy.get("[data-testid='next-page']").click();
cy.wait("@getProducts");        // page-2 fetch
 
cy.get("[data-testid='next-page']").click();
cy.wait("@getProducts");        // page-3 fetch

Each cy.wait("@getProducts") waits for a new matching request. Cypress tracks how many you've consumed and pauses for the next. This is the cleanest way to test paginated lists — no cy.wait(ms) between pages, no race conditions on slow CI.

For tests where the order matters and you want to assert on a specific occurrence, you can index into the captured requests with cy.get("@alias.all") (every interception so far) or cy.get("@alias.<n>") (the nth):

cy.get("@getProducts.all").should("have.length", 3);    // three page loads happened
cy.get("@getProducts.0").its("response.body.page").should("eq", 1);
cy.get("@getProducts.2").its("response.body.page").should("eq", 3);

Custom timeouts on cy.wait

The default cy.wait timeout is 5 seconds (set by requestTimeout and responseTimeout in cypress.config.ts). For genuinely slow endpoints — analytics aggregations, report generation — bump it per call:

cy.intercept("POST", "/api/reports/generate").as("generateReport");
cy.get("[data-testid='generate-btn']").click();
cy.wait("@generateReport", { timeout: 30000 });   // up to 30s
cy.get("[data-testid='report-link']").should("be.visible");

The same surgical-vs-global rule from the auto-waiting lesson applies: prefer per-call { timeout: ... } to bumping the global timeout. Failing tests should fail fast everywhere except the one place that genuinely needs slack.

A real three-step checkout

Stitching everything together into the kind of multi-step flow you'll write at work — a checkout that fires three distinct API calls in sequence:

describe("Three-step checkout", () => {
  beforeEach(() => {
    cy.intercept("POST", "/api/orders").as("createOrder");
    cy.intercept("POST", "/api/payments").as("processPayment");
    cy.intercept("PATCH", "/api/orders/*").as("confirmOrder");
 
    cy.visit("/cart");
  });
 
  it("places an order through three sequential API calls", () => {
    cy.get("[data-testid='checkout-btn']").click();
    cy.wait("@createOrder").then((i) => {
      expect(i.response?.statusCode).to.equal(201);
      expect(i.response?.body).to.have.property("id");
    });
 
    cy.get("[data-testid='card-number']").type("4242424242424242");
    cy.get("[data-testid='pay-btn']").click();
    cy.wait("@processPayment").its("response.statusCode").should("eq", 200);
 
    cy.get("[data-testid='confirm-btn']").click();
    cy.wait("@confirmOrder").then((i) => {
      expect(i.request.body).to.have.property("status", "confirmed");
      expect(i.response?.statusCode).to.equal(200);
    });
 
    cy.get("[data-testid='confirmation']")
      .should("contain", "Thank you for your order");
  });
});

Three intercepts registered up front. Three user actions. Three aliased waits, each gated on the previous step completing. The test runs as fast as the network allows — and asserts both that each request fired and that each response was right.

The aliased-wait timeline

Step 1 of 5

Register intercepts

Three cy.intercept(...).as('alias') calls in beforeEach. Cypress installs listeners for createOrder, processPayment, confirmOrder.

⚠️ Common mistakes

  • Calling cy.wait("@alias") twice without a second matching request happening. Each cy.wait("@alias") consumes one occurrence. If you call it twice but the page only fired one request, the second wait hangs and times out. When you wait twice on the same alias, make sure the action between waits actually triggered another matching request.
  • Aliasing inside the action callback instead of before it. A pattern like cy.get(...).click().then(() => cy.intercept(...).as("foo")) registers the intercept after the click that triggers the request. The intercept never fires. Always register in beforeEach (or before the action), then act, then wait.
  • Mixing cy.wait(2000) with cy.wait("@alias"). New engineers often hedge — they put cy.wait(2000) in and the aliased wait, "just in case." Drop the fixed wait. The alias is the entire mechanism; adding a sleep beside it does nothing useful and breaks the speed argument for using aliases at all.

🎯 Practice task

Build an aliased-wait test for a real multi-step flow. 25-30 minutes.

  1. In your scaffolded project, target a public CRUD demo such as https://jsonplaceholder.typicode.com (it accepts POST/PUT/DELETE that return realistic responses without persisting). Set baseUrl accordingly.
  2. Create cypress/e2e/aliases.cy.ts and write a test that:
    • Registers three intercepts in beforeEach for GET /posts, POST /posts, and DELETE /posts/* — alias them @listPosts, @createPost, @deletePost.
    • In a single it block, fires those three requests (you can do it via cy.request for now if your test app doesn't have a UI for them — but the assertion pattern is identical for app-driven requests).
    • Waits on each alias and asserts the correct status code at each step.
  3. Pagination drill — visit a page that loads ten items per fetch (or stub a single intercept that fires multiple times). Click "Load more" three times and cy.wait("@listPosts") after each click. Assert the visible item count grows by 10 each time.
  4. Index into past requests — after the three pagination waits, use cy.get("@listPosts.all").should("have.length", 4) to confirm the load-more triggered four total fetches (initial + 3 clicks). Inspect cy.get("@listPosts.0") and assert the first response was page 1.
  5. Fast vs sleep comparison — pick an aliased-wait test, replicate it with cy.wait(5000) instead, and run both 10 times. Note the duration delta. The aliased version should be 5–8x faster.
  6. Stretch: wire a cy.wait("@createPost", { timeout: 15000 }) on an endpoint you've stubbed with delay: 8000. Confirm it passes. Drop the timeout option (defaulting back to 5000ms) and confirm it now times out. This calibrates the per-call timeout pattern in your head.

You now have aliases, intercepts, and stubs — the full network toolkit for the browser side of testing. The next lesson swaps to the other side: cy.request, the command for making your own HTTP calls from the test, used for setup, login, and direct API testing.

// tip to track lessons you complete and pick up where you left off across devices.