Most QA engineers think of Playwright as a UI testing tool. But here's what they're missing: Playwright can test APIs as easily as it tests UIs—and you don't need Postman, REST Client, or any separate tool.
This changes everything. You can now test your entire system—API + UI—in a single framework, with a single language, from a single test file.
On the AI Sales Assistant project I worked on, we tested:
- ✅ API endpoints (authentication, data retrieval, mutations)
- ✅ API → UI integration (API response displays correctly in UI)
- ✅ Error handling (API failures show proper UI errors)
All from one Playwright test file. No Postman. No separate test runners. Just clean, maintainable tests.
Why Test APIs with Playwright?
Traditional approach: Use Postman for API testing, Selenium/Playwright for UI testing. Two frameworks, two languages, two sets of reports.
Playwright approach: One framework for everything.
- ✅ Single test language (TypeScript)
- ✅ Unified reporting
- ✅ Reuse test data across API + UI tests
- ✅ Test API → UI workflows in sequence
- ✅ No context switching between tools
Basic API Testing: GET Request
import { test, expect } from '@playwright/test';
test('should fetch user data via GET request', async ({ request }) => {
// Make GET request
const response = await request.get('https://api.example.com/users/1');
// Assert response status
expect(response.status()).toBe(200);
// Parse JSON
const user = await response.json();
// Assert response data
expect(user).toHaveProperty('id');
expect(user.id).toBe(1);
expect(user.name).toBe('John Doe');
expect(user.email).toBe('john@example.com');
});
Authenticated API Requests
Most APIs require authentication. Here's the pattern:
import { test, expect } from '@playwright/test';
test('should fetch protected endpoint with auth token', async ({ request }) => {
// Step 1: Get auth token
const loginResponse = await request.post('https://api.example.com/auth/login', {
data: {
email: 'user@example.com',
password: 'password123'
}
});
const { token } = await loginResponse.json();
// Step 2: Use token in subsequent requests
const response = await request.get('https://api.example.com/users/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.status()).toBe(200);
const profile = await response.json();
expect(profile.name).toBe('John Doe');
});
API Fixture for Reuse Across Tests
Create a fixture for common API interactions:
// fixtures/apiClient.ts
import { APIRequestContext } from '@playwright/test';
export class APIClient {
private token: string | null = null;
private baseURL = 'https://api.example.com';
constructor(private request: APIRequestContext) {}
// Login and store token
async login(email: string, password: string) {
const response = await this.request.post(`${this.baseURL}/auth/login`, {
data: { email, password }
});
const { token } = await response.json();
this.token = token;
}
// GET with auth header
async get(endpoint: string) {
return await this.request.get(`${this.baseURL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
}
// POST with auth header
async post(endpoint: string, data: any) {
return await this.request.post(`${this.baseURL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.token}`
},
data
});
}
// DELETE with auth header
async delete(endpoint: string) {
return await this.request.delete(`${this.baseURL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
}
}
// Register fixture
import { test } from '@playwright/test';
test.extend({
apiClient: async ({ request }, use) => {
const client = new APIClient(request);
// Login before test
await client.login('user@example.com', 'password123');
await use(client);
}
});
Using the API Fixture in Tests
import { test, expect } from '../fixtures/apiClient';
test.describe('User API', () => {
test('should fetch user profile', async ({ apiClient }) => {
const response = await apiClient.get('/users/profile');
expect(response.status()).toBe(200);
const profile = await response.json();
expect(profile.name).toBeDefined();
});
test('should update user email', async ({ apiClient }) => {
const response = await apiClient.post('/users/profile', {
email: 'newemail@example.com'
});
expect(response.status()).toBe(200);
const updated = await response.json();
expect(updated.email).toBe('newemail@example.com');
});
});
Testing API → UI Integration
Here's the power of Playwright: test your API and UI together:
import { test, expect } from '@playwright/test'; test('should create user via API and verify in UI', async ({ page, request }) => { // Step 1: Create user via API const createResponse = await request.post('https://api.example.com/users', { data: { name: 'Test User', email: 'test@example.com', role: 'admin' } }); expect(createResponse.status()).toBe(201); const newUser = await createResponse.json(); // Step 2: Navigate to user list in UI await page.goto('/users'); // Step 3: Verify the created user appears in UI const userRow = page.locator(`text=${newUser.name}`); await expect(userRow).toBeVisible(); // Step 4: Click to view user details await userRow.click(); // Step 5: Verify details match API response await expect(page.locator('[data-testid="user-email"]')).toContainText(newUser.email); await expect(page.locator('[data-testid="user-role"]')).toContainText(newUser.role); });Error Handling: Testing Bad Requests
test('should handle 400 Bad Request gracefully', async ({ request }) => { // Missing required field const response = await request.post('https://api.example.com/users', { data: { name: 'Test User' // email is missing } }); expect(response.status()).toBe(400); const error = await response.json(); expect(error.message).toContain('email is required'); }); test('should handle 401 Unauthorized', async ({ request }) => { // No auth token const response = await request.get('https://api.example.com/users/profile'); expect(response.status()).toBe(401); const error = await response.json(); expect(error.message).toContain('unauthorized'); }); test('should handle 404 Not Found', async ({ request }) => { const response = await request.get('https://api.example.com/users/999999'); expect(response.status()).toBe(404); const error = await response.json(); expect(error.message).toContain('not found'); });API Mocking: Test Without Real Backend
Sometimes you want to test UI behavior without calling the real API. Use request mocking:
test('should show error when API fails', async ({ page }) => { // Mock API to return error await page.route('**/api/users/**', route => { route.abort('failed'); // Simulate network failure }); // Try to fetch user await page.goto('/users/1'); // Verify error message displayed await expect(page.locator('[role="alert"]')).toContainText('Failed to load user'); }); test('should handle slow API gracefully', async ({ page }) => { // Mock API to be slow await page.route('**/api/users/**', route => { setTimeout(() => { route.continue(); }, 5000); // 5 second delay }); await page.goto('/users/1'); // Verify loading indicator shows await expect(page.locator('[data-testid="loading"]')).toBeVisible(); // Wait for data to load await expect(page.locator('[data-testid="user-name"]')).toBeVisible({ timeout: 10000 }); });Testing GraphQL APIs
The same pattern works for GraphQL:
test('should query user via GraphQL', async ({ request }) => { const response = await request.post('https://api.example.com/graphql', { data: { query: ` query GetUser($id: ID!) { user(id: $id) { id name email } } `, variables: { id: '1' } } }); expect(response.status()).toBe(200); const { data } = await response.json(); expect(data.user.name).toBe('John Doe'); }); test('should handle GraphQL errors', async ({ request }) => { const response = await request.post('https://api.example.com/graphql', { data: { query: 'query { invalid }' // Invalid query } }); const { errors } = await response.json(); expect(errors).toBeDefined(); expect(errors[0].message).toContain('Cannot query field'); });Real Project Example: AI Sales Assistant
On the AI Sales Assistant project, we tested:
test('should qualify lead via API and show in CRM', async ({ page, request, apiClient }) => { // Step 1: Create lead via API const leadResponse = await apiClient.post('/leads', { name: 'Prospect Inc', email: 'contact@prospect.com', companySize: 'enterprise' }); const lead = await leadResponse.json(); // Step 2: AI qualification happens on backend await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for async processing // Step 3: Verify qualification result via API const qualificationResponse = await apiClient.get(`/leads/${lead.id}/qualification`); const qualification = await qualificationResponse.json(); expect(qualification.qualificationScore).toBeGreaterThan(0.8); expect(qualification.recommendedNextStep).toBe('demo'); // Step 4: Navigate to CRM and verify lead appears with qualification await page.goto('/crm/leads'); const leadRow = page.locator(`text=${lead.name}`); await expect(leadRow).toBeVisible(); // Verify qualification badge const badge = leadRow.locator('[data-testid="qualification-badge"]'); await expect(badge).toContainText('Hot Lead'); });Best Practices for API Testing
- Isolate test data: Use unique email addresses per test to avoid conflicts
- Clean up after tests: Delete created data via API after assertions
- Use fixtures for setup: Login once in a fixture, reuse across tests
- Test at API boundaries: Invalid inputs, missing fields, wrong types
- Verify both status + body: HTTP 200 doesn't mean the response is correct
- Use assertions for important fields: Don't just check response.ok
Frequently Asked Questions
Is API testing with Playwright slower than Postman?
Slightly slower due to Playwright's overhead, but typically 10-50ms difference per request. The unified framework benefit outweighs the minor speed trade-off.
Can I test file uploads with Playwright API?
Yes, using FormData for multipart/form-data requests. Slightly more complex than JSON, but fully supported.
Should I use Playwright for all API testing or keep Postman?
Use Playwright for integration tests (API + UI), keep Postman for manual exploration and API documentation. They're complementary, not competing.
How do I handle rate limiting in tests?
Add delays between requests, use test data factories to minimize API calls, or mock the API in tests where appropriate.
Next Steps
Start testing your APIs with Playwright today. You'll immediately notice:
- ✅ Less context switching between tools
- ✅ Unified test reporting
- ✅ Ability to test end-to-end workflows (API → UI)
- ✅ Single language for all testing
This is the future of QA testing: one framework for everything.
Need help setting up API testing with Playwright? I offer complete API automation consulting.
Let's build your comprehensive API + UI test suite →
Related Articles:
Tayyab Akmal
AI & QA Automation Engineer
I've caught critical bugs in fintech, e-commerce, and SaaS platforms — then built the automation that prevents them from shipping again. 6+ years scaling test automation and AI-driven QA.