Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

API Testing with Playwright: Full Guide with Examples (2026)

March 22, 2026 EST. READ: 13 MIN #Quality Assurance

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

  1. Isolate test data: Use unique email addresses per test to avoid conflicts
  2. Clean up after tests: Delete created data via API after assertions
  3. Use fixtures for setup: Login once in a fixture, reuse across tests
  4. Test at API boundaries: Invalid inputs, missing fields, wrong types
  5. Verify both status + body: HTTP 200 doesn't mean the response is correct
  6. 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
// author

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.

// related_dispatches

YOU MIGHT ALSO READ

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire