🕒 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.
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,DELETEoperations - Use comprehensive
expectassertions 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