# Playwright API Automation

> Implement API test automation using Playwright's built-in APIRequestContext. Use when: testing REST APIs alongside UI, sharing auth state between API and UI tests, mocking backend responses, validating API contracts.

- Skill: `bavithiranhardy14/playwright-api-automation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add bavithiranhardy14/playwright-api-automation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bavithiranhardy14/playwright-api-automation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: bavithiranhardy14 (https://skillmd.com/u/bavithiranhardy14)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/bavithiranhardy14/playwright-api-automation

---


# Playwright API Automation

## When to Use

- Testing REST APIs using the same Playwright setup as UI tests
- Sharing authenticated sessions between API and UI tests
- Seeding test data via API before UI tests
- Validating API responses alongside browser interactions
- Mocking API responses in UI tests

## Why Playwright for API Testing

- **No extra library** — built into Playwright (`APIRequestContext`)
- **Shared auth state** — same cookies/tokens used by browser and API calls
- **Integrated** — seed data via API, verify via UI in the same test
- **Fast** — API calls without browser overhead

---

## Procedure

### 1. Create API Client

**services/playwright_api_client.py**

```python
from playwright.sync_api import APIRequestContext, Playwright
from utils.logger import get_logger
from config.settings import config
import json

logger = get_logger(__name__)


class PlaywrightAPIClient:
    """API client using Playwright's APIRequestContext."""

    def __init__(self, request: APIRequestContext):
        self.request = request
        self.base_url = config.api_base_url

    def get(self, endpoint: str, params: dict = None, headers: dict = None):
        url = f"{self.base_url}{endpoint}"
        response = self.request.get(url, params=params, headers=headers)
        logger.info(f"GET {url} → {response.status}")
        return response

    def post(self, endpoint: str, payload: dict = None, headers: dict = None):
        url = f"{self.base_url}{endpoint}"
        response = self.request.post(url, data=json.dumps(payload), headers=headers)
        logger.info(f"POST {url} → {response.status}")
        return response

    def put(self, endpoint: str, payload: dict = None, headers: dict = None):
        url = f"{self.base_url}{endpoint}"
        response = self.request.put(url, data=json.dumps(payload), headers=headers)
        logger.info(f"PUT {url} → {response.status}")
        return response

    def delete(self, endpoint: str, headers: dict = None):
        url = f"{self.base_url}{endpoint}"
        response = self.request.delete(url, headers=headers)
        logger.info(f"DELETE {url} → {response.status}")
        return response

    def assert_status(self, response, expected: int):
        assert response.status == expected, (
            f"Expected {expected}, got {response.status}. Body: {response.text()}"
        )

    def json(self, response):
        return response.json()
```

---

### 2. API Fixtures

**fixtures/api_fixtures.py**

```python
import pytest
from playwright.sync_api import sync_playwright
from services.playwright_api_client import PlaywrightAPIClient
from config.settings import config


@pytest.fixture(scope="session")
def api_request_context():
    """Session-level Playwright API request context."""
    with sync_playwright() as pw:
        context = pw.request.new_context(
            base_url=config.api_base_url,
            extra_http_headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
            }
        )
        yield context
        context.dispose()


@pytest.fixture(scope="session")
def api_client(api_request_context):
    """Provide PlaywrightAPIClient for the test session."""
    return PlaywrightAPIClient(api_request_context)


@pytest.fixture(scope="session")
def authenticated_api_client():
    """API client with auth token from login endpoint."""
    with sync_playwright() as pw:
        context = pw.request.new_context(base_url=config.api_base_url)
        
        # Authenticate and capture token
        response = context.post(
            "/auth/login",
            data='{"username": "admin", "password": "pass123"}',
            headers={"Content-Type": "application/json"}
        )
        assert response.status == 200
        token = response.json()["access_token"]
        context.dispose()

        # New context with auth header
        auth_context = pw.request.new_context(
            base_url=config.api_base_url,
            extra_http_headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            }
        )
        yield PlaywrightAPIClient(auth_context)
        auth_context.dispose()
```

---

### 3. API Test Examples

**tests/api/test_users_api.py**

```python
import pytest
from playwright.sync_api import expect

@pytest.mark.api
class TestUsersAPI:
    """REST API tests using Playwright APIRequestContext."""

    @pytest.mark.smoke
    def test_get_all_users(self, api_client):
        response = api_client.get("/users")
        api_client.assert_status(response, 200)
        
        data = api_client.json(response)
        assert isinstance(data, list)
        assert len(data) > 0

    @pytest.mark.smoke
    def test_create_user(self, authenticated_api_client):
        payload = {
            "name": "Test User",
            "email": "testuser@example.com",
            "role": "viewer"
        }
        response = authenticated_api_client.post("/users", payload)
        authenticated_api_client.assert_status(response, 201)
        
        data = authenticated_api_client.json(response)
        assert data["email"] == payload["email"]
        assert "id" in data

    @pytest.mark.regression
    def test_get_user_by_id(self, api_client):
        response = api_client.get("/users/1")
        api_client.assert_status(response, 200)
        
        data = api_client.json(response)
        assert data["id"] == 1
        assert "name" in data
        assert "email" in data

    @pytest.mark.regression
    def test_update_user(self, authenticated_api_client):
        update_payload = {"name": "Updated Name"}
        response = authenticated_api_client.put("/users/1", update_payload)
        authenticated_api_client.assert_status(response, 200)
        
        data = authenticated_api_client.json(response)
        assert data["name"] == "Updated Name"

    @pytest.mark.regression
    def test_delete_user(self, authenticated_api_client):
        response = authenticated_api_client.delete("/users/99")
        authenticated_api_client.assert_status(response, 204)

    def test_get_nonexistent_user(self, api_client):
        response = api_client.get("/users/999999")
        api_client.assert_status(response, 404)
```

---

### 4. API + UI Combined Test

Seed test data via API, then verify through the browser UI:

**tests/test_api_ui_combined.py**

```python
import pytest

@pytest.mark.integration
class TestAPIAndUI:
    """Tests that combine API setup with UI verification."""

    def test_create_user_via_api_then_verify_in_ui(self, authenticated_api_client, page):
        # STEP 1 — Create user via API (fast, reliable)
        response = authenticated_api_client.post("/users", {
            "name": "E2E Test User",
            "email": "e2e_user@example.com"
        })
        authenticated_api_client.assert_status(response, 201)
        user_id = authenticated_api_client.json(response)["id"]

        # STEP 2 — Verify user appears in UI
        pw_page = page.page
        pw_page.goto(f"https://app.example.com/users/{user_id}")
        pw_page.wait_for_load_state("networkidle")

        from playwright.sync_api import expect
        expect(pw_page.get_by_text("E2E Test User")).to_be_visible()

    def test_delete_via_api_then_verify_ui_404(self, authenticated_api_client, page):
        # STEP 1 — Delete via API
        authenticated_api_client.delete("/users/50")

        # STEP 2 — Verify 404 page in UI
        pw_page = page.page
        pw_page.goto("https://app.example.com/users/50")
        expect(pw_page.get_by_text("Not Found")).to_be_visible()
```

---

### 5. Mock API in UI Tests

Use Playwright routing to intercept and mock API calls during UI tests:

```python
def test_dashboard_with_mocked_api(page):
    pw_page = page.page

    # Intercept API and return mock data
    pw_page.route("**/api/dashboard/stats", lambda route: route.fulfill(
        status=200,
        content_type="application/json",
        body='{"users": 150, "revenue": 45000, "orders": 320}'
    ))

    pw_page.goto("https://app.example.com/dashboard")

    from playwright.sync_api import expect
    expect(pw_page.get_by_text("150")).to_be_visible()
    expect(pw_page.get_by_text("45000")).to_be_visible()


def test_error_state_with_mocked_api_failure(page):
    pw_page = page.page

    # Simulate API failure
    pw_page.route("**/api/users", lambda route: route.fulfill(
        status=500,
        body='{"error": "Internal Server Error"}'
    ))

    pw_page.goto("https://app.example.com/users")
    from playwright.sync_api import expect
    expect(pw_page.get_by_text("Something went wrong")).to_be_visible()
```

---

### 6. Run API Tests

```bash
# Run all API tests
pytest tests/api/ -m api

# Run combined API + UI tests
pytest tests/ -m integration

# Run with verbose output
pytest tests/api/ -v --tb=short

# Run with HTML report
pytest tests/api/ --html=reports/api_report.html
```

## Best Practices

- Use `authenticated_api_client` for protected endpoints
- Use API to **seed data** before UI tests — faster and more reliable
- Use `page.route()` to **mock APIs** when testing UI error states
- Always call `context.dispose()` in fixture teardown
- Share session-level API context to avoid re-auth per test

