API-Driven Test Setup — Seeding Data, Creating State and Bypassing the UI

The single highest-impact optimisation for a Cypress test suite is replacing UI-driven setup with API-driven setup. When your test needs an order to exist before verifying the order details page, creating that order via cy.request('POST', '/api/orders', data) takes milliseconds. Creating it by navigating through the shop, adding items, filling checkout forms, and submitting — the UI route — takes 10-20 seconds. Across 50 tests, that difference is 8 minutes versus 2 seconds of setup time.

API-Driven Setup — Fast, Reliable Test Preconditions

The principle is simple: use the UI only for the feature you are testing. Set up everything else via API.

// ── PATTERN: API setup + UI verification ──

describe('Order Details Page', () => {
  let orderId: string;

  beforeEach(() => {
    // Step 1: Authenticate via API (not UI login form)
    cy.apiLogin('standard_user', 'secret_sauce');

    // Step 2: Create test order via API (not checkout flow)
    cy.request('POST', '/api/orders', {
      items: [
        { productId: 1, name: 'Backpack', price: 29.99, quantity: 1 },
        { productId: 2, name: 'Bike Light', price: 9.99, quantity: 2 },
      ],
      shipping: { method: 'standard', cost: 5.99 },
    }).then((response) => {
      expect(response.status).to.eq(201);
      orderId = response.body.id;
    });
  });

  it('should display order with correct item count', () => {
    // UI verification — this is what we are ACTUALLY testing
    cy.visit(`/orders/${orderId}`);
    cy.get('[data-cy="order-items"]').should('have.length', 2);
  });

  it('should display correct total with shipping', () => {
    cy.visit(`/orders/${orderId}`);
    // 29.99 + (9.99 * 2) + 5.99 = 55.96
    cy.get('[data-cy="order-total"]').should('contain.text', '$55.96');
  });

  it('should show pending status for new orders', () => {
    cy.visit(`/orders/${orderId}`);
    cy.get('[data-cy="order-status"]').should('contain.text', 'Pending');
  });
});


// ── Common API setup patterns ──

// Create a user with specific attributes
Cypress.Commands.add('createTestUser', (overrides = {}) => {
  const defaults = {
    name: `TestUser_${Date.now()}`,
    email: `test_${Date.now()}@example.com`,
    password: 'TestPass1!',
    role: 'customer',
  };
  const userData = { ...defaults, ...overrides };

  return cy.request('POST', '/api/test/users', userData)
    .its('body');
});


// Seed a product catalogue
Cypress.Commands.add('seedProducts', (count: number = 5) => {
  const products = Array.from({ length: count }, (_, i) => ({
    name: `Test Product ${i + 1}`,
    price: parseFloat((Math.random() * 100 + 1).toFixed(2)),
    inStock: true,
  }));

  return cy.request('POST', '/api/test/seed-products', { products })
    .its('body');
});


// Reset the database to a known state
Cypress.Commands.add('resetDatabase', () => {
  return cy.request('POST', '/api/test/reset');
});


// ── Usage in tests ──
/*
describe('Product Catalogue', () => {
  beforeEach(() => {
    cy.resetDatabase();
    cy.seedProducts(10);
    cy.apiLogin('standard_user', 'secret_sauce');
  });

  it('should display all seeded products', () => {
    cy.visit('/shop');
    cy.get('.product-card').should('have.length', 10);
  });
});
*/


// ── Setup time comparison ──
const SETUP_COMPARISON = {
  'Login via UI':        '3-5 seconds (visit, type, click, wait for redirect)',
  'Login via API':       '50-200 milliseconds (single POST request)',
  'Create order via UI': '10-20 seconds (5 pages of navigation and forms)',
  'Create order via API':'100-300 milliseconds (single POST request)',
  'Suite: 50 tests':     'UI setup: 5-15 min overhead | API setup: 5-10 sec overhead',
};
Note: The ...overrides pattern in createTestUser is essential for flexible test data. Default values ensure the command works without arguments (cy.createTestUser()), while overrides let specific tests customise what they need: cy.createTestUser({ role: 'admin' }). This pattern avoids both hardcoded data (inflexible) and required parameters (verbose). Every API setup command should provide sensible defaults with optional overrides.
Tip: Use timestamps or random suffixes in generated test data to avoid unique-constraint conflicts: `test_${Date.now()}@example.com`. When tests run in parallel or are retried without cleanup, fixed data like test@example.com causes “email already exists” errors. Timestamped data is unique per invocation and eliminates this entire category of parallel execution failures.
Warning: API-driven setup requires the application to have test-friendly API endpoints. If your application does not expose endpoints for creating users, seeding data, or resetting state, you need to work with the development team to add them. These endpoints should be protected (available only in test environments, not production) and clearly documented. Without them, you are forced to use slow UI-driven setup or direct database manipulation — both inferior to clean API setup.

Common Mistakes

Mistake 1 — Using UI-driven setup for every test precondition

❌ Wrong: Every test navigates through 5 pages to create an order before testing the order details page — 50 tests waste 10+ minutes on repeated checkout flows.

✅ Correct: cy.request('POST', '/api/orders', data) in beforeEach() — order created in milliseconds, test focuses on verifying the details page.

Mistake 2 — Using fixed test data that conflicts in parallel execution

❌ Wrong: Every test creates a user with email test@example.com — parallel tests get “email already exists” errors.

✅ Correct: email: `test_${Date.now()}@example.com` — unique per invocation, no conflicts.

🧠 Test Yourself

A test needs to verify that the order history page shows 5 orders. What is the most efficient way to set up this test?