
API Testing with Cypress
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.
Table Of Contents-
Two Approaches to API Testing
Cypress provides two commands for working with APIs:
| Command | Purpose | Use Case |
|---|---|---|
cy.request() | Makes actual HTTP requests | Direct API testing, test setup, authentication |
cy.intercept() | Spies on, stubs, or modifies requests | E2E 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 callStubbing 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?