Skip to main content

API Testing Overview

API tests in this framework validate REST endpoints using Cypress’s cy.request() command. Unlike UI tests, API tests don’t use page objects - they interact directly with HTTP endpoints.

Basic API Test Structure

describe('API Requests - GET', () => {
  it('Should receive a 200 status response', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).its('status').should('eq', 200)
  })
});

API Test Anatomy

1

Describe Block

Groups related API tests together (e.g., all GET requests).
2

Request

Uses cy.request() to make HTTP calls to the API.
3

Assertions

Validates response status, headers, body, and performance.

Complete API Test Example

Here’s a real test suite from the framework:
GETSpec.cy.ts
/// <reference types="cypress" />

describe('API Requests - GET', () => {
  it('Should receive a 200 status response', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).its('status').should('eq', 200)
  })

  it('Should last no more than 1 second', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).then((res) => {
      expect(res.duration).to.be.lessThan(1000)
    })
  })

  it('Should have the correct headers', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).its('headers').its('content-type')
      .should('include', 'application/json')
  })

  it('Should return an array', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).then((res) => {
      expect(res.body).to.be.an('array')
    })
  })

  it('Should have a bookingid as property for each one', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).then((res) => {
      res.body.forEach(item => {
        expect(item).to.have.property('bookingid').that.is.a('number')
      })
    })
  })

  it('Should return at least 1 result', () => {
    cy.request({
      method: 'GET',
      url: '/booking',
    }).then((res) => {
      expect(res.body).to.not.be.empty
    })
  })
});

Request Configuration

The cy.request() command accepts various options:
cy.request({
  method: 'GET',
  url: '/booking/1',
})

Request Options

  • method: HTTP method (GET, POST, PUT, DELETE, PATCH)
  • url: Endpoint URL (relative to baseUrl in config)
  • body: Request payload for POST/PUT/PATCH
  • headers: Custom HTTP headers
  • qs: Query string parameters
  • auth: Basic authentication
  • timeout: Request timeout in milliseconds

Response Validation

Status Code Validation

it('Should return 200 for successful GET', () => {
  cy.request('GET', '/booking')
    .its('status')
    .should('eq', 200)
})

it('Should return 404 for non-existent resource', () => {
  cy.request({
    method: 'GET',
    url: '/booking/99999',
    failOnStatusCode: false  // Don't fail test on 4xx/5xx
  }).its('status').should('eq', 404)
})

Header Validation

it('Should have correct content-type header', () => {
  cy.request('/booking')
    .its('headers')
    .its('content-type')
    .should('include', 'application/json')
})

it('Should have CORS headers', () => {
  cy.request('/booking').then((res) => {
    expect(res.headers).to.have.property('access-control-allow-origin')
  })
})

Body Validation

it('Should return array of bookings', () => {
  cy.request('/booking').then((res) => {
    expect(res.body).to.be.an('array')
    expect(res.body).to.not.be.empty
  })
})

it('Should have required properties', () => {
  cy.request('/booking/1').then((res) => {
    expect(res.body).to.have.property('firstname')
    expect(res.body).to.have.property('lastname')
    expect(res.body).to.have.property('totalprice')
    expect(res.body.totalprice).to.be.a('number')
  })
})

Performance Validation

it('Should respond within 1 second', () => {
  cy.request('/booking').then((res) => {
    expect(res.duration).to.be.lessThan(1000)
  })
})

it('Should respond within 500ms for cached data', () => {
  cy.request({
    method: 'GET',
    url: '/booking/1',
    headers: {
      'Cache-Control': 'max-age=3600'
    }
  }).then((res) => {
    expect(res.duration).to.be.lessThan(500)
  })
})
The duration property in the response shows how long the request took in milliseconds.

Testing with Fixtures

Use fixtures to validate API responses against expected data:
bookings.json
[
  {
    "firstname": "James",
    "lastname": "Brown",
    "totalprice": 111,
    "depositpaid": true,
    "bookingdates": {
      "checkin": "2018-01-01",
      "checkout": "2019-01-01"
    },
    "additionalneeds": "Breakfast"
  }
]
Using in tests:
it('Should return specific booking data', () => {
  cy.fixture('bookings').then((bookingData: any) => {
    for (let i = 1; i < 4; i++) {
      cy.request({
        method: 'GET',
        url: `/booking/${i}`,
      }).then((res) => {
        const { firstname, lastname, totalprice, depositpaid } = res.body
        expect(firstname).to.equal(bookingData[i - 1].firstname)
        expect(lastname).to.equal(bookingData[i - 1].lastname)
        expect(totalprice).to.equal(bookingData[i - 1].totalprice)
        expect(depositpaid).to.equal(bookingData[i - 1].depositpaid)
      })
    }
  })
})
This approach works well for controlled test environments. For public APIs, you may need to use more flexible assertions.

Advanced API Testing Patterns

Pattern 1: Request Chaining

it('Should create and retrieve booking', () => {
  let bookingId: number

  // Create booking
  cy.request({
    method: 'POST',
    url: '/booking',
    body: {
      firstname: 'Test',
      lastname: 'User',
      totalprice: 100,
      depositpaid: true,
      bookingdates: {
        checkin: '2024-01-01',
        checkout: '2024-01-10'
      }
    }
  }).then((res) => {
    expect(res.status).to.eq(200)
    bookingId = res.body.bookingid

    // Retrieve created booking
    cy.request(`/booking/${bookingId}`).then((getRes) => {
      expect(getRes.body.firstname).to.eq('Test')
      expect(getRes.body.lastname).to.eq('User')
    })
  })
})

Pattern 2: Authentication Flow

it('Should authenticate and access protected endpoint', () => {
  let authToken: string

  // Get auth token
  cy.request({
    method: 'POST',
    url: '/auth',
    body: {
      username: 'admin',
      password: 'password123'
    }
  }).then((res) => {
    authToken = res.body.token

    // Use token for authenticated request
    cy.request({
      method: 'DELETE',
      url: '/booking/1',
      headers: {
        'Cookie': `token=${authToken}`
      }
    }).its('status').should('eq', 201)
  })
})

Pattern 3: Data-Driven Testing

it('Should handle multiple booking queries', () => {
  const queries = [
    { firstname: 'John', lastname: 'Doe' },
    { firstname: 'Jane', lastname: 'Smith' },
    { firstname: 'Bob', lastname: 'Johnson' }
  ]

  queries.forEach(query => {
    cy.request({
      method: 'GET',
      url: '/booking',
      qs: query
    }).then((res) => {
      expect(res.status).to.eq(200)
      expect(res.body).to.be.an('array')
    })
  })
})

Pattern 4: Error Handling

it('Should handle invalid request gracefully', () => {
  cy.request({
    method: 'POST',
    url: '/booking',
    body: {
      // Missing required fields
      firstname: 'Test'
    },
    failOnStatusCode: false
  }).then((res) => {
    expect(res.status).to.be.oneOf([400, 500])
    expect(res.body).to.have.property('error')
  })
})

Schema Validation

Validate response structure using Cypress assertions:
it('Should match booking schema', () => {
  cy.request('/booking/1').then((res) => {
    const booking = res.body
    
    // Validate top-level properties
    expect(booking).to.have.all.keys(
      'firstname',
      'lastname',
      'totalprice',
      'depositpaid',
      'bookingdates',
      'additionalneeds'
    )
    
    // Validate nested object
    expect(booking.bookingdates).to.have.all.keys('checkin', 'checkout')
    
    // Validate types
    expect(booking.firstname).to.be.a('string')
    expect(booking.totalprice).to.be.a('number')
    expect(booking.depositpaid).to.be.a('boolean')
  })
})

Testing Different HTTP Methods

it('Should retrieve resource', () => {
  cy.request('GET', '/booking/1')
    .its('status')
    .should('eq', 200)
})

Best Practices

Keep tests focused on a single assertion:
// ✅ Good - focused test
it('Should return 200 status', () => {
  cy.request('/booking').its('status').should('eq', 200)
})

it('Should return array', () => {
  cy.request('/booking').then(res => {
    expect(res.body).to.be.an('array')
  })
})

// ❌ Bad - testing multiple things
it('Should work correctly', () => {
  cy.request('/booking').then(res => {
    expect(res.status).to.eq(200)
    expect(res.body).to.be.an('array')
    expect(res.headers['content-type']).to.include('json')
  })
})
Set failOnStatusCode: false when testing error cases:
it('Should return 404 for invalid ID', () => {
  cy.request({
    method: 'GET',
    url: '/booking/invalid',
    failOnStatusCode: false
  }).its('status').should('eq', 404)
})
Include performance assertions:
it('Should respond quickly', () => {
  cy.request('/booking').then(res => {
    expect(res.duration).to.be.lessThan(2000)
  })
})
Delete created resources after tests:
it('Should create and clean up booking', () => {
  cy.request('POST', '/booking', { /* data */ })
    .then(res => {
      const id = res.body.bookingid
      
      // Test logic here
      
      // Clean up
      cy.request({
        method: 'DELETE',
        url: `/booking/${id}`,
        headers: { 'Cookie': 'token=abc' }
      })
    })
})

Running API Tests

npm run cy:open:api
API tests run much faster than UI tests since they don’t involve browser rendering.

Next Steps

Using Fixtures

Manage API test data with fixtures

Running Tests

Learn all test execution options

Test Organization

Structure your API test suite

Maintainability

Keep API tests maintainable

Build docs developers (and LLMs) love