Cypress’s cy.request is a powerful tool for testing REST APIs directly, bypassing the browser and its DOM.
Here’s a simple POST request to create a user and verify the response:
it('creates a user via API', () => {
cy.request({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/users',
body: {
name: 'Cypress User',
username: 'cy_user',
email: 'cy.user@example.com'
}
}).then((response) => {
expect(response.status).to.eq(201);
expect(response.body).to.have.property('name', 'Cypress User');
expect(response.body).to.have.property('username', 'cy_user');
expect(response.body).to.have.property('email', 'cy.user@example.com');
});
});
This example demonstrates the core cy.request usage: specifying the method, url, and body for the request, and then using .then() to access the response object. The response object contains crucial properties like status (the HTTP status code) and body (the parsed JSON response from the server).
cy.request is invaluable for testing APIs because it allows you to:
- Isolate API logic: Test your backend services independently of your frontend. This means faster, more stable tests that don’t rely on the UI rendering correctly.
- Seed test data: Create or modify data directly through your API before running UI tests. This ensures your UI tests operate on known, predictable data states.
- Validate API responses: Assert that your API returns the correct status codes, data structures, and values.
- Test authentication and authorization: Simulate different user roles or token expirations to ensure your API handles access control correctly.
- Handle complex request/response cycles: Chain multiple API requests to simulate multi-step processes, like creating a resource, updating it, and then deleting it.
Internally, cy.request uses Node.js’s http or https modules to make the requests. It handles details like setting appropriate Content-Type headers for JSON payloads and parsing JSON responses automatically. This abstraction means you don’t need to worry about the low-level networking details.
The url property can be a string or an object. When it’s an object, you can specify url, method, body, headers, qs (for query string parameters), auth, and failOnStatusCode.
it('fetches a specific user and checks headers', () => {
cy.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/users',
qs: { id: 1 }, // Equivalent to ?id=1 in the URL
headers: {
'X-Custom-Header': 'MyValue'
},
failOnStatusCode: false // Don't fail the test if status code is not 2xx/3xx
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body[0]).to.have.property('id', 1);
expect(response.headers['content-type']).to.include('application/json');
});
});
The qs option is particularly useful for constructing URLs with query parameters without manually building the string. The headers option allows you to send custom headers, which is essential for API key authentication or setting Accept types. failOnStatusCode: false is handy when you specifically want to test error responses (e.g., 404, 401) and assert on them.
You can also use cy.request to send form-encoded data, which is common for older APIs or certain authentication flows.
it('submits a form via API', () => {
cy.request({
method: 'POST',
url: 'https://httpbin.org/post',
form: true, // Set Content-Type to application/x-www-form-urlencoded
body: {
username: 'testuser',
password: 'password123'
}
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.form).to.deep.equal({
username: 'testuser',
password: 'password123'
});
});
});
By setting form: true, Cypress automatically sets the Content-Type header to application/x-www-form-urlencoded and encodes the body accordingly. This is a subtle but critical detail for many backend systems.
One common pattern is to use cy.request to log a user in and then use the returned token in subsequent UI tests.
let authToken;
before(() => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'user@example.com',
password: 'password'
}
}).then(response => {
authToken = response.body.token;
});
});
it('visits protected page after login', () => {
// Use the token to set authorization header for subsequent requests
cy.request({
url: '/api/user/profile',
headers: {
Authorization: `Bearer ${authToken}`
}
}).then(response => {
expect(response.status).to.eq(200);
// ... more assertions
});
});
This approach decouples authentication from the UI login flow, making tests faster and more robust. You can even use the auth option for basic HTTP authentication.
When making requests that are part of a larger test suite, it’s often beneficial to alias the response. This makes your .then() blocks cleaner and allows you to reference the aliased response in other commands or tests.
it('creates a resource and aliases the response', () => {
cy.request('POST', '/api/resources', { name: 'New Resource' })
.then((response) => {
expect(response.status).to.eq(201);
return response.body.id; // Return data to the next .then()
})
.as('resourceId'); // Alias the returned value
cy.get('@resourceId').then((id) => {
cy.request('GET', `/api/resources/${id}`).then((getResponse) => {
expect(getResponse.status).to.eq(200);
expect(getResponse.body.id).to.equal(id);
});
});
});
The .as() command is incredibly versatile for passing data between cy.request calls or even between API and UI commands.
A common oversight when dealing with cy.request is how it interacts with Cypress’s built-in retryability. While UI commands automatically retry on failure, cy.request commands themselves do not retry by default if the request itself fails (e.g., network error). However, assertions within the .then() block are retried. If you need to retry the entire cy.request call based on its response, you’ll need to implement custom retry logic or use a loop.
The next step is often integrating cy.request with your UI tests to pre-populate data or verify backend changes after UI interactions.