API Testing with Playwright (ReqRes API)

Master API testing with Playwright's built-in request context and comprehensive assertions

🕒 Teaching Flow

1. Story Intro (5 min)

👉 "Playwright isn't just for UI — it can also test APIs. In fact, many teams now build hybrid tests: API for setup/validation, UI for workflows. We'll learn GET, POST, PUT, DELETE with reqres.in today."
API Testing Analogy:
Think of APIs as the kitchen of a restaurant (backend) and UI as the dining room (frontend). Testing APIs directly is like checking if the kitchen can prepare dishes correctly, while UI testing is like checking if customers get the right food at their table.

Setup (5 min)

No extra library needed — Playwright has built-in request context!

📂 Basic Setup

import { test, expect, request } from "@playwright/test";

// That's it! No additional dependencies required.
Key Benefits:
  • Built-in request context - no external libraries
  • Same assertion library as UI tests
  • Can combine API and UI testing in same test
  • Automatic cookie and session management

GET Request (List Users)

GET https://reqres.in/api/users?page=2

📂 tests/api-get-users.spec.js

import { test, expect, request } from "@playwright/test";

test("GET - List Users", async ({ request }) => {
  const response = await request.get("https://reqres.in/api/users?page=2");

  // Status and response validation
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  const resBody = await response.json();
  console.log("Response Body:", resBody);

  // Response structure validation
  expect(resBody.page).toBe(2);
  expect(resBody.data.length).toBeGreaterThan(0);
  expect(resBody.data[0]).toHaveProperty("email");
  expect(resBody.data[0]).toHaveProperty("first_name");
  expect(resBody.data[0]).toHaveProperty("last_name");
  expect(resBody.data[0]).toHaveProperty("avatar");

  // Validate specific user data
  const firstUser = resBody.data[0];
  expect(firstUser.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  expect(firstUser.first_name).toBeTruthy();
  expect(firstUser.last_name).toBeTruthy();
  expect(firstUser.avatar).toMatch(/^https?:\/\/.+/);

  // Validate pagination
  expect(resBody.per_page).toBe(6);
  expect(resBody.total).toBeGreaterThan(0);
  expect(resBody.total_pages).toBeGreaterThan(0);
});

📋 Expected Response Structure

{
  "page": 2,
  "per_page": 6,
  "total": 12,
  "total_pages": 2,
  "data": [
    {
      "id": 7,
      "email": "michael.lawson@reqres.in",
      "first_name": "Michael",
      "last_name": "Lawson",
      "avatar": "https://reqres.in/img/faces/7-image.jpg"
    }
  ]
}

POST Request (Create User)

POST https://reqres.in/api/users

📂 tests/api-create-user.spec.js

import { test, expect, request } from "@playwright/test";

test("POST - Create User", async ({ request }) => {
  const userData = {
    name: "QA Tester",
    job: "Automation Engineer"
  };

  const response = await request.post("https://reqres.in/api/users", {
    data: userData,
    headers: {
      'Content-Type': 'application/json'
    }
  });

  // Status validation
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(201);

  const resBody = await response.json();
  console.log("Created User:", resBody);

  // Response validation
  expect(resBody.name).toBe("QA Tester");
  expect(resBody.job).toBe("Automation Engineer");
  expect(resBody).toHaveProperty("id");
  expect(resBody).toHaveProperty("createdAt");
  
  // Validate ID format
  expect(typeof resBody.id).toBe("string");
  expect(resBody.id.length).toBeGreaterThan(0);
  
  // Validate timestamp
  expect(new Date(resBody.createdAt)).toBeInstanceOf(Date);
  expect(new Date(resBody.createdAt).getTime()).toBeLessThanOrEqual(Date.now());
});

📋 Expected Response Structure

{
  "name": "QA Tester",
  "job": "Automation Engineer",
  "id": "123",
  "createdAt": "2024-01-15T10:30:00.000Z"
}

PUT Request (Update User)

PUT https://reqres.in/api/users/2

📂 tests/api-update-user.spec.js

import { test, expect, request } from "@playwright/test";

test("PUT - Update User", async ({ request }) => {
  const updateData = {
    name: "QA Tester Updated",
    job: "Lead QA"
  };

  const response = await request.put("https://reqres.in/api/users/2", {
    data: updateData,
    headers: {
      'Content-Type': 'application/json'
    }
  });

  // Status validation
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  const resBody = await response.json();
  console.log("Updated User:", resBody);

  // Response validation
  expect(resBody.name).toBe("QA Tester Updated");
  expect(resBody.job).toBe("Lead QA");
  expect(resBody).toHaveProperty("updatedAt");
  
  // Validate timestamp
  expect(new Date(resBody.updatedAt)).toBeInstanceOf(Date);
  expect(new Date(resBody.updatedAt).getTime()).toBeLessThanOrEqual(Date.now());
});

DELETE Request (Remove User)

DELETE https://reqres.in/api/users/2

📂 tests/api-delete-user.spec.js

import { test, expect, request } from "@playwright/test";

test("DELETE - Remove User", async ({ request }) => {
  const response = await request.delete("https://reqres.in/api/users/2");

  // Status validation
  expect(response.status()).toBe(204);
  expect(response.ok()).toBeTruthy();
  
  // Verify no content in response
  const responseText = await response.text();
  expect(responseText).toBe("");
  
  // Verify response headers
  const contentType = response.headers()['content-type'];
  expect(contentType).toBeFalsy(); // No content type for 204
});

Chaining API + UI (Hybrid Testing)

🔄 API + UI Integration Flow

📂 tests/hybrid-api-ui.spec.js

import { test, expect, request } from "@playwright/test";

test("API + UI Flow - Create User and Validate", async ({ request, page }) => {
  // Step 1: Create user via API
  const apiRes = await request.post("https://reqres.in/api/users", {
    data: { 
      name: "Hybrid Tester", 
      job: "QA Developer" 
    }
  });

  expect(apiRes.ok()).toBeTruthy();
  expect(apiRes.status()).toBe(201);

  const body = await apiRes.json();
  const userId = body.id;
  const userName = body.name;

  expect(userId).toBeTruthy();
  expect(userName).toBe("Hybrid Tester");

  console.log("✅ API User Created:", { userId, userName });

  // Step 2: Use in UI test (navigate to ReqRes site)
  await page.goto("https://reqres.in/");
  await page.waitForLoadState('networkidle');
  
  // Verify page loaded correctly
  await expect(page).toHaveTitle(/Reqres/);
  await expect(page.locator('h1')).toContainText('Test your front-end against a real API');

  // Step 3: Navigate to users page and verify API data consistency
  await page.click('text=List users');
  await page.waitForLoadState('networkidle');
  
  // Verify users are displayed
  await expect(page.locator('.user-item')).toHaveCount.greaterThan(0);
  
  console.log("✅ Hybrid Test Completed - API + UI Integration Successful");
});

test("API Setup + UI Validation", async ({ request, page }) => {
  // Step 1: Get user data via API
  const apiRes = await request.get("https://reqres.in/api/users/2");
  expect(apiRes.ok()).toBeTruthy();
  
  const userData = await apiRes.json();
  const userEmail = userData.data.email;
  const userFirstName = userData.data.first_name;
  
  console.log("📋 API User Data:", { userEmail, userFirstName });

  // Step 2: Navigate to UI and validate data consistency
  await page.goto("https://reqres.in/");
  await page.click('text=Single user');
  await page.waitForLoadState('networkidle');
  
  // Verify user data is displayed correctly in UI
  await expect(page.locator('.user-email')).toContainText(userEmail);
  await expect(page.locator('.user-name')).toContainText(userFirstName);
  
  console.log("✅ API-UI Data Consistency Verified");
});

Advanced API Testing Patterns

Error Handling & Negative Testing

📂 tests/api-error-handling.spec.js

import { test, expect, request } from "@playwright/test";

test("GET - Handle 404 Error", async ({ request }) => {
  const response = await request.get("https://reqres.in/api/users/999");
  
  expect(response.status()).toBe(404);
  expect(response.ok()).toBeFalsy();
  
  const errorBody = await response.json();
  expect(errorBody).toEqual({});
});

test("POST - Handle Invalid Data", async ({ request }) => {
  const response = await request.post("https://reqres.in/api/users", {
    data: {
      // Missing required fields
      invalidField: "test"
    }
  });
  
  // API might still return 201 but with default values
  expect(response.status()).toBe(201);
  
  const resBody = await response.json();
  expect(resBody).toHaveProperty("id");
  expect(resBody).toHaveProperty("createdAt");
});

test("PUT - Handle Non-existent User", async ({ request }) => {
  const response = await request.put("https://reqres.in/api/users/999", {
    data: {
      name: "Test User",
      job: "Test Job"
    }
  });
  
  expect(response.status()).toBe(200);
  const resBody = await response.json();
  expect(resBody.name).toBe("Test User");
  expect(resBody.job).toBe("Test Job");
});

Data-Driven API Testing

📂 tests/api-data-driven.spec.js

import { test, expect, request } from "@playwright/test";

const testUsers = [
  { name: "John Doe", job: "Developer" },
  { name: "Jane Smith", job: "Designer" },
  { name: "Bob Johnson", job: "Manager" }
];

test.describe('Data-Driven User Creation', () => {
  testUsers.forEach((user, index) => {
    test(`Create user ${index + 1}: ${user.name}`, async ({ request }) => {
      const response = await request.post("https://reqres.in/api/users", {
        data: user
      });
      
      expect(response.ok()).toBeTruthy();
      expect(response.status()).toBe(201);
      
      const resBody = await response.json();
      expect(resBody.name).toBe(user.name);
      expect(resBody.job).toBe(user.job);
      expect(resBody).toHaveProperty("id");
      expect(resBody).toHaveProperty("createdAt");
      
      console.log(`✅ Created user: ${user.name} with ID: ${resBody.id}`);
    });
  });
});

🧑‍💻 Interactive Questions & Answers

1. What's the difference between .ok() and .status()?

Answer:
  • .ok(): Returns boolean - true for status codes 200-299, false otherwise
  • .status(): Returns the exact HTTP status code as a number
  • Use .ok() for: Quick success/failure checks
  • Use .status() for: Specific status code validation
  • Example:
    // Quick check
    expect(response.ok()).toBeTruthy();
    
    // Specific validation
    expect(response.status()).toBe(201);

2. When to use data vs form in request body?

Answer:
  • data: For JSON payloads (most common for APIs)
  • form: For form-encoded data (application/x-www-form-urlencoded)
  • Use data for: REST APIs, JSON APIs, modern web services
  • Use form for: Traditional form submissions, legacy APIs
  • Example:
    // JSON API
    await request.post('/api/users', {
      data: { name: 'John', job: 'Developer' }
    });
    
    // Form submission
    await request.post('/submit', {
      form: { name: 'John', job: 'Developer' }
    });

3. Why is DELETE returning 204 and not 200?

Answer:
  • 204 No Content: Standard response for successful DELETE operations
  • Indicates: Request was successful but no content to return
  • 200 OK: Would imply there's content to return
  • HTTP Standards: 204 is the correct status for successful deletions
  • Best Practice: Always expect 204 for DELETE operations

4. How would you validate the response schema (keys/values)?

Answer:
  • Property validation: Use toHaveProperty() for required fields
  • Type validation: Use typeof checks for data types
  • Structure validation: Use toEqual() for exact object matching
  • Pattern validation: Use toMatch() for string patterns
  • Example:
    const response = await response.json();
    
    // Property existence
    expect(response).toHaveProperty("id");
    expect(response).toHaveProperty("name");
    
    // Type validation
    expect(typeof response.id).toBe("string");
    expect(typeof response.createdAt).toBe("string");
    
    // Pattern validation
    expect(response.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
    
    // Structure validation
    expect(response).toEqual({
      name: "John Doe",
      job: "Developer",
      id: expect.any(String),
      createdAt: expect.any(String)
    });

5. Can we reuse API responses between multiple UI tests? How?

Answer:
  • Yes, absolutely! API responses can be shared across tests
  • Methods: Store in variables, use fixtures, or global setup
  • Benefits: Faster test execution, consistent test data
  • Example:
    // Method 1: Store in variable
    let createdUserId;
    
    test("Create user via API", async ({ request }) => {
      const response = await request.post("/api/users", {
        data: { name: "Test User", job: "QA" }
      });
      createdUserId = (await response.json()).id;
    });
    
    test("Use created user in UI", async ({ page }) => {
      await page.goto(`/users/${createdUserId}`);
      await expect(page.locator('.user-name')).toContainText("Test User");
    });
    
    // Method 2: Use fixtures
    test("API + UI with fixtures", async ({ apiUser, page }) => {
      await page.goto(`/users/${apiUser.id}`);
      await expect(page.locator('.user-name')).toContainText(apiUser.name);
    });

6. How do you handle authentication in API tests?

Answer:
  • Bearer tokens: Add Authorization header with token
  • API keys: Include in headers or query parameters
  • Session cookies: Use request context with cookies
  • Example:
    // Bearer token
    await request.get("/api/protected", {
      headers: {
        'Authorization': 'Bearer your-token-here'
      }
    });
    
    // API key
    await request.get("/api/data", {
      headers: {
        'X-API-Key': 'your-api-key'
      }
    });
    
    // Session cookies
    const context = await request.newContext({
      extraHTTPHeaders: {
        'Cookie': 'session=abc123'
      }
    });

7. How do you test API performance and response times?

Answer:
  • Response time measurement: Use Date.now() before and after request
  • Performance assertions: Validate response times meet requirements
  • Load testing: Run multiple concurrent requests
  • Example:
    test("API Performance Test", async ({ request }) => {
      const startTime = Date.now();
      
      const response = await request.get("/api/users");
      
      const endTime = Date.now();
      const responseTime = endTime - startTime;
      
      expect(response.ok()).toBeTruthy();
      expect(responseTime).toBeLessThan(1000); // Less than 1 second
      
      console.log(`Response time: ${responseTime}ms`);
    });

8. How do you handle API rate limiting in tests?

Answer:
  • Rate limit detection: Check for 429 status codes
  • Retry logic: Implement exponential backoff
  • Test isolation: Use delays between requests
  • Example:
    async function makeRequestWithRetry(request, url, maxRetries = 3) {
      for (let i = 0; i < maxRetries; i++) {
        const response = await request.get(url);
        
        if (response.status() === 429) {
          const delay = Math.pow(2, i) * 1000; // Exponential backoff
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
        
        return response;
      }
      throw new Error('Max retries exceeded');
    }

🎯 Day 6 Homework Tasks

🟢 Beginner

Task 1

Write a GET test for https://reqres.in/api/users/2 → assert first name, last name, and email format.

Task 2

Write a POST test that creates a user → assert id exists and createdAt is valid timestamp.

🟡 Intermediate

Task 3

Write a PUT test that updates user job → assert job = "Senior QA" and updatedAt is recent.

Task 4

Write a DELETE test → assert status = 204 and response body is empty.

🔴 Advanced

Task 5

Combine: Create user via POST → Update via PUT → Delete → Assert all steps succeed with proper data validation.

Task 6

Write a hybrid test: Create user via API → Navigate to UI → Validate user appears in user list.

Best Practices & Tips

API Testing Best Practices:
  • Always validate both status codes and response structure
  • Use meaningful test data that represents real scenarios
  • Implement proper error handling for negative test cases
  • Use data-driven testing for multiple test scenarios
  • Combine API and UI testing for comprehensive coverage
Common Pitfalls:
  • Don't forget to validate response headers and content types
  • Avoid hardcoded test data - use dynamic or parameterized data
  • Don't ignore authentication requirements in API tests
  • Be careful with rate limiting - implement proper delays
  • Always clean up test data after test execution
Performance & Reliability Tips:
  • Measure and validate API response times
  • Implement retry logic for flaky API calls
  • Use proper timeout configurations
  • Test both success and failure scenarios
  • Validate data consistency between API and UI

✅ Outcomes

  • Test APIs with Playwright using built-in request context
  • Master GET, POST, PUT, DELETE operations
  • Use comprehensive expect assertions for all response validations
  • Understand how API + UI can work together in hybrid testing
  • Implement error handling and negative testing scenarios
  • Apply data-driven testing patterns for API automation
  • Handle authentication and performance testing
  • Follow best practices for maintainable API test automation