UI Automation
Cypress
API Testing

API Testing with Cypress

Parul Dhingra - Senior Quality Analyst
Parul Dhingra13+ Years ExperienceHire Me

Senior Quality Analyst

Updated: 1/23/2026

Cypress excels at API testing, whether you're testing APIs directly or controlling network behavior in E2E tests. With cy.request() for making actual HTTP requests and cy.intercept() for spying on and stubbing network traffic, you have complete control over the network layer.

This guide covers both approaches: testing APIs themselves and using API control to write faster, more reliable UI tests.

Two Approaches to API Testing

Cypress provides two commands for working with APIs:

CommandPurposeUse Case
cy.request()Makes actual HTTP requestsDirect API testing, test setup, authentication
cy.intercept()Spies on, stubs, or modifies requestsE2E tests, mocking, controlling responses

When to Use Each

Use cy.request() when:

  • Testing API endpoints directly
  • Setting up test data before UI tests
  • Logging in without going through UI
  • Making requests that bypass the browser

Use cy.intercept() when:

  • Waiting for application's network requests
  • Stubbing responses to control test scenarios
  • Testing error handling (500 errors, timeouts)
  • Speeding up tests by avoiding real API calls

cy.request() - Direct API Calls

cy.request() makes HTTP requests from Cypress (not from the browser), allowing you to test APIs or set up test state efficiently.

Basic GET Request

cy.request('/api/users').then((response) => {
  expect(response.status).to.eq(200)
  expect(response.body).to.have.length(5)
})

Full Options

cy.request({
  method: 'GET',
  url: '/api/users',
  headers: {
    Authorization: 'Bearer token123',
    'Content-Type': 'application/json',
  },
  qs: {
    // Query string parameters
    page: 1,
    limit: 10,
  },
  timeout: 10000, // Request timeout
  failOnStatusCode: false, // Don't fail on 4xx/5xx
}).then((response) => {
  // Assertions
})

POST Request

cy.request({
  method: 'POST',
  url: '/api/users',
  body: {
    name: 'John Doe',
    email: 'john@example.com',
    role: 'admin',
  },
}).then((response) => {
  expect(response.status).to.eq(201)
  expect(response.body).to.have.property('id')
  expect(response.body.name).to.eq('John Doe')
})

PUT/PATCH Requests

// PUT - Full update
cy.request({
  method: 'PUT',
  url: '/api/users/123',
  body: {
    name: 'Updated Name',
    email: 'updated@example.com',
  },
}).then((response) => {
  expect(response.status).to.eq(200)
})
 
// PATCH - Partial update
cy.request({
  method: 'PATCH',
  url: '/api/users/123',
  body: {
    name: 'New Name',
  },
})

DELETE Request

cy.request({
  method: 'DELETE',
  url: '/api/users/123',
}).then((response) => {
  expect(response.status).to.eq(204)
})

Response Object Properties

cy.request('/api/users').then((response) => {
  // Status
  expect(response.status).to.eq(200)
  expect(response.statusText).to.eq('OK')
 
  // Headers
  expect(response.headers).to.have.property('content-type')
  expect(response.headers['content-type']).to.include('application/json')
 
  // Body
  expect(response.body).to.be.an('array')
  expect(response.body[0]).to.have.all.keys('id', 'name', 'email')
 
  // Duration
  expect(response.duration).to.be.lessThan(1000)
})

Chaining Requests

// Create user, then fetch them
cy.request('POST', '/api/users', { name: 'John' })
  .then((createResponse) => {
    const userId = createResponse.body.id
 
    return cy.request(`/api/users/${userId}`)
  })
  .then((getResponse) => {
    expect(getResponse.body.name).to.eq('John')
  })

Using Aliases

cy.request('/api/users').its('body').as('users')
 
// Later in the test
cy.get('@users').then((users) => {
  expect(users).to.have.length.greaterThan(0)
})

cy.intercept() - Network Control

cy.intercept() doesn't make requests - it intercepts requests that your application makes, allowing you to spy on, wait for, stub, or modify them.

Basic Interception (Spy)

// Spy on GET requests to /api/users
cy.intercept('GET', '/api/users').as('getUsers')
 
// Trigger the request
cy.visit('/users')
 
// Wait for it to complete
cy.wait('@getUsers')

Intercept Syntax

// Method and URL
cy.intercept('GET', '/api/users')
cy.intercept('POST', '/api/users')
 
// URL only (matches any method)
cy.intercept('/api/users')
 
// URL pattern with wildcard
cy.intercept('GET', '/api/users/*') // /api/users/123
cy.intercept('GET', '/api/**/users') // /api/v1/users, /api/v2/users
cy.intercept('GET', '**/users') // Any path ending in /users
 
// Options object
cy.intercept({
  method: 'GET',
  url: '/api/users',
  hostname: 'api.example.com',
})

Route Matcher Options

cy.intercept({
  method: 'GET',
  url: '/api/users',
  hostname: 'api.example.com',
  pathname: '/api/users',
  port: 3000,
  https: true,
  query: {
    page: '1',
  },
  headers: {
    'content-type': 'application/json',
  },
}).as('apiCall')

Waiting for Requests

Wait for Single Request

cy.intercept('GET', '/api/users').as('getUsers')
cy.get('[data-cy="load-users"]').click()
cy.wait('@getUsers')
 
// Continue after request completes
cy.get('[data-cy="user-list"]').should('be.visible')

Assert on Intercepted Request/Response

cy.intercept('POST', '/api/users').as('createUser')
 
cy.get('[data-cy="submit"]').click()
 
cy.wait('@createUser').then((interception) => {
  // Request assertions
  expect(interception.request.body).to.deep.equal({
    name: 'John',
    email: 'john@test.com',
  })
  expect(interception.request.headers).to.have.property('authorization')
 
  // Response assertions
  expect(interception.response.statusCode).to.eq(201)
  expect(interception.response.body).to.have.property('id')
})

Wait for Multiple Requests

cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/settings').as('getSettings')
 
cy.visit('/dashboard')
 
// Wait for both
cy.wait(['@getUsers', '@getSettings'])
 
// Or wait for them individually
cy.wait('@getUsers')
cy.wait('@getSettings')

Wait Multiple Times

// Wait for the same endpoint multiple times
cy.intercept('GET', '/api/notifications').as('getNotifications')
 
cy.wait('@getNotifications') // First call
cy.wait('@getNotifications') // Second call

Stubbing Responses

Static Response

// Stub with inline body
cy.intercept('GET', '/api/users', {
  body: [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
  ],
}).as('getUsers')
 
// Stub with status code
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  body: [],
})
 
// Stub with headers
cy.intercept('GET', '/api/users', {
  statusCode: 200,
  headers: {
    'X-Custom-Header': 'value',
  },
  body: { data: [] },
})

Using Fixtures

// cypress/fixtures/users.json
// [{ "id": 1, "name": "John" }, { "id": 2, "name": "Jane" }]
 
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')

Delayed Response

// Simulate slow API
cy.intercept('GET', '/api/users', {
  delay: 2000, // 2 second delay
  body: { users: [] },
}).as('slowUsers')
 
// Test loading states
cy.get('[data-cy="load"]').click()
cy.get('[data-cy="spinner"]').should('be.visible')
cy.wait('@slowUsers')
cy.get('[data-cy="spinner"]').should('not.exist')

Throttled Response

cy.intercept('GET', '/api/large-file', {
  throttleKbps: 100, // Limit to 100 KB/s
  body: largeData,
})

Modifying Requests and Responses

Modify Request

cy.intercept('POST', '/api/users', (req) => {
  // Add header
  req.headers['X-Test-Header'] = 'test-value'
 
  // Modify body
  req.body.timestamp = Date.now()
  req.body.source = 'cypress-test'
 
  // Continue with modified request
  req.continue()
})

Modify Response

cy.intercept('GET', '/api/users', (req) => {
  req.reply((res) => {
    // Modify status
    res.statusCode = 200
 
    // Modify body
    res.body.push({ id: 999, name: 'Injected User' })
 
    // Modify headers
    res.headers['x-modified'] = 'true'
  })
})

Conditional Responses

cy.intercept('GET', '/api/users/*', (req) => {
  const userId = req.url.split('/').pop()
 
  if (userId === '1') {
    req.reply({ id: 1, name: 'Admin', role: 'admin' })
  } else if (userId === '2') {
    req.reply({ id: 2, name: 'User', role: 'user' })
  } else {
    req.reply({ statusCode: 404, body: { error: 'Not found' } })
  }
})

Dynamic Fixture Selection

cy.intercept('GET', '/api/users', (req) => {
  const page = req.query.page || '1'
  req.reply({ fixture: `users-page-${page}.json` })
})

Testing API Errors

HTTP Error Codes

// 404 Not Found
cy.intercept('GET', '/api/users/999', {
  statusCode: 404,
  body: { error: 'User not found' },
}).as('notFound')
 
// 500 Server Error
cy.intercept('POST', '/api/users', {
  statusCode: 500,
  body: { error: 'Internal Server Error' },
}).as('serverError')
 
// 401 Unauthorized
cy.intercept('GET', '/api/private', {
  statusCode: 401,
  body: { error: 'Unauthorized' },
}).as('unauthorized')
 
// 403 Forbidden
cy.intercept('DELETE', '/api/admin/*', {
  statusCode: 403,
  body: { error: 'Forbidden' },
}).as('forbidden')
 
// 422 Validation Error
cy.intercept('POST', '/api/users', {
  statusCode: 422,
  body: {
    errors: [{ field: 'email', message: 'Email already exists' }],
  },
}).as('validationError')

Network Errors

// Force network error
cy.intercept('GET', '/api/users', { forceNetworkError: true }).as(
  'networkError',
)
 
cy.get('[data-cy="load"]').click()
cy.get('[data-cy="error-message"]').should('contain', 'Network error')

Test Error UI

describe('Error Handling', () => {
  it('displays error message on server error', () => {
    cy.intercept('GET', '/api/users', {
      statusCode: 500,
      body: { error: 'Server Error' },
    }).as('serverError')
 
    cy.visit('/users')
    cy.wait('@serverError')
 
    cy.get('[data-cy="error-alert"]')
      .should('be.visible')
      .and('contain', 'Something went wrong')
 
    cy.get('[data-cy="retry-button"]').should('be.visible')
  })
 
  it('shows not found message for missing resource', () => {
    cy.intercept('GET', '/api/users/999', {
      statusCode: 404,
    }).as('notFound')
 
    cy.visit('/users/999')
    cy.wait('@notFound')
 
    cy.get('[data-cy="not-found"]').should('contain', 'User not found')
  })
})

Authentication Patterns

API Login (Fast)

// Custom command for API login
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password },
  }).then((response) => {
    // Store token in localStorage
    window.localStorage.setItem('authToken', response.body.token)
 
    // Or set cookie
    cy.setCookie('auth_token', response.body.token)
  })
})
 
// Use in tests
beforeEach(() => {
  cy.loginByApi('user@test.com', 'password123')
  cy.visit('/dashboard')
})

Token Injection for Intercepted Requests

// Add auth header to all API requests
cy.intercept('/api/**', (req) => {
  const token = localStorage.getItem('authToken')
  if (token) {
    req.headers['Authorization'] = `Bearer ${token}`
  }
})

Test Authenticated Endpoints

describe('Protected API', () => {
  beforeEach(() => {
    cy.loginByApi('user@test.com', 'password')
  })
 
  it('fetches user profile', () => {
    cy.request({
      method: 'GET',
      url: '/api/me',
      headers: {
        Authorization: `Bearer ${localStorage.getItem('authToken')}`,
      },
    }).then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body.email).to.eq('user@test.com')
    })
  })
})

Session Preservation

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    experimentalSessionAndOrigin: true,
  },
})
 
// In tests - sessions are cached and reused
cy.session(
  'user',
  () => {
    cy.loginByApi('user@test.com', 'password')
  },
  {
    validate() {
      cy.request('/api/me').its('status').should('eq', 200)
    },
  },
)

Best Practices

1. Use Intercepts for Waiting, Not Timeouts

// Bad - arbitrary wait
cy.get('[data-cy="submit"]').click()
cy.wait(3000)
 
// Good - wait for specific request
cy.intercept('POST', '/api/submit').as('submit')
cy.get('[data-cy="submit"]').click()
cy.wait('@submit')

2. Stub External Services

// Stub third-party APIs
cy.intercept('GET', 'https://api.stripe.com/**', {
  statusCode: 200,
  body: { success: true },
})
 
cy.intercept('POST', 'https://maps.googleapis.com/**', {
  fixture: 'google-maps-response.json',
})

3. Test Happy Path and Error States

describe('User Creation', () => {
  it('creates user successfully', () => {
    cy.intercept('POST', '/api/users', {
      statusCode: 201,
      body: { id: 1, name: 'John' },
    }).as('createUser')
 
    cy.get('[data-cy="name"]').type('John')
    cy.get('[data-cy="submit"]').click()
 
    cy.wait('@createUser')
    cy.get('[data-cy="success-message"]').should('be.visible')
  })
 
  it('handles validation errors', () => {
    cy.intercept('POST', '/api/users', {
      statusCode: 422,
      body: { errors: [{ field: 'name', message: 'Required' }] },
    }).as('createUser')
 
    cy.get('[data-cy="submit"]').click()
 
    cy.wait('@createUser')
    cy.get('[data-cy="error-name"]').should('contain', 'Required')
  })
})

4. Use cy.request() for Setup

beforeEach(() => {
  // Reset database
  cy.request('POST', '/api/test/reset')
 
  // Create test data
  cy.request('POST', '/api/users', {
    name: 'Test User',
    email: 'test@example.com',
  })
 
  // Login via API (faster than UI)
  cy.loginByApi('test@example.com', 'password')
})

5. Organize Intercepts in Fixtures

// cypress/fixtures/intercepts/users.js
export const mockUsers = [
  { id: 1, name: 'John', email: 'john@test.com' },
  { id: 2, name: 'Jane', email: 'jane@test.com' },
]
 
export const setupUserIntercepts = () => {
  cy.intercept('GET', '/api/users', { body: mockUsers }).as('getUsers')
  cy.intercept('POST', '/api/users', { statusCode: 201 }).as('createUser')
  cy.intercept('DELETE', '/api/users/*', { statusCode: 204 }).as('deleteUser')
}
 
// In test
import { setupUserIntercepts } from '../fixtures/intercepts/users'
 
beforeEach(() => {
  setupUserIntercepts()
})

6. Assert Request Body in cy.intercept()

cy.intercept('POST', '/api/orders', (req) => {
  // Validate request before responding
  expect(req.body).to.have.property('items')
  expect(req.body.items).to.have.length.greaterThan(0)
  expect(req.body.items[0]).to.have.property('productId')
 
  req.reply({ statusCode: 201, body: { orderId: '12345' } })
}).as('createOrder')

cy.request() vs cy.intercept(): Remember, cy.request() makes requests from Cypress (for setup/API testing). cy.intercept() watches requests your app makes (for controlling E2E test scenarios). They serve different purposes and are often used together.


Test Your Knowledge

Quiz on Cypress API Testing

Your Score: 0/10

Question: What is the key difference between cy.request() and cy.intercept()?


Continue Learning


Frequently Asked Questions

Frequently Asked Questions (FAQs) / People Also Ask (PAA)

Can I use cy.intercept() to spy on requests made by cy.request()?

How do I handle authentication in API tests?

How do I test API rate limiting?

Can I test file uploads via cy.request()?

How do I wait for multiple API calls to complete?

How do I simulate a slow API response?

What's the difference between stubbing and spying in cy.intercept()?

How do I test GraphQL APIs with Cypress?