API Testing with Playwright: HTTP Testing Alongside Browser Automation
Playwright's API testing works in JavaScript, TypeScript, Python, Java, and C#. The examples in this guide use TypeScript, which is the most common choice for Playwright projects, but the concepts and patterns translate directly to other languages. The Playwright documentation includes full API reference for each language binding.
The key advantage of using Playwright for API testing is consolidation. Instead of maintaining Postman collections for API tests, a Playwright project for browser tests, and a separate framework for assertion logic, everything lives in one codebase with one test runner, one configuration file, one CI pipeline, and one reporting system. Teams that already use Playwright for end-to-end testing can add API tests with zero additional tooling.
Create an API Request Context
Playwright provides API testing capability through the APIRequestContext class. You can access it through the built-in request fixture in test functions, or create one manually with playwright.request.newContext() for more control over configuration.
The simplest approach uses the request fixture that Playwright provides to every test. In your playwright.config.ts, set a baseURL under the use section: use: { baseURL: 'https://api.example.com' }. Then in your test file, the request fixture is available automatically and uses this base URL for all requests. This fixture creates a fresh context for each test, ensuring test isolation.
For custom configuration, create an APIRequestContext manually. This lets you set default headers (like Authorization tokens), custom timeout values, proxy configuration, and HTTP credentials. The context is reusable across multiple requests, so you configure authentication once and every request inherits it. Create the context in a beforeAll hook and dispose of it in afterAll to manage its lifecycle properly.
When you need multiple contexts with different configurations (for example, one authenticated as an admin and another as a regular user), create separate contexts in your test setup. Each context maintains its own cookies, headers, and base URL, so requests made through different contexts simulate different users without interfering with each other.
The request context respects Playwright's global configuration including proxy settings, custom SSL certificates, and HTTP/2 support. This makes it suitable for testing APIs behind corporate firewalls, APIs using self-signed certificates in staging environments, and APIs that require HTTP/2 for certain features like server push.
Send Requests and Assert Responses
The APIRequestContext provides methods for every HTTP verb: get(), post(), put(), patch(), delete(), head(), and fetch() (a general-purpose method that accepts any HTTP method). Each method accepts a URL (relative to the base URL or absolute), optional request options (headers, body, query parameters, form data, multipart data, timeout), and returns an APIResponse object.
A basic GET test sends a request and asserts the response status and body. Call request.get('/api/users/1') to send the request. The returned response object provides ok() for a boolean success check, status() for the numeric status code, json() for parsed JSON body, text() for raw text body, headers() for response headers, and headersArray() for headers that appear multiple times. Use Playwright's expect() for assertions: expect(response.ok()).toBeTruthy() checks for 2xx status, expect(response.status()).toBe(200) checks the exact status code, and the JSON body can be destructured and asserted field by field.
POST requests include a request body. Call request.post('/api/users', { data: { name: 'John', email: 'john@example.com' } }). Playwright automatically serializes the data object to JSON and sets the Content-Type header to application/json. For form-encoded data, use the form option instead: request.post('/api/login', { form: { username: 'admin', password: 'secret' } }). For multipart/form-data (file uploads), use the multipart option with Buffer or stream values.
Response body assertions work with the parsed JSON. Call const body = await response.json() to get the parsed object, then assert individual fields: expect(body.name).toBe('John'), expect(body.id).toBeDefined(), expect(body.email).toContain('@'). For array responses, assert the length and contents: expect(body).toHaveLength(10), expect(body[0]).toHaveProperty('id'). Playwright's expect API includes matchers like toContain(), toHaveProperty(), toMatchObject(), and toEqual() that work naturally with API response data.
Header assertions verify that the API returns correct metadata. Access headers with response.headers() which returns a plain object with lowercase header names. Assert content type: expect(response.headers()['content-type']).toContain('application/json'). Check cache headers: expect(response.headers()['cache-control']).toBe('no-store'). Verify custom headers like rate limit information: expect(response.headers()['x-ratelimit-remaining']).toBeDefined().
Response timing assertions catch performance regressions. While Playwright does not expose response timing directly in the APIResponse, you can measure it with simple timing around the request call. Record Date.now() before the request, send the request, record Date.now() after, and assert that the difference is within your threshold. For more precise timing, use Playwright's tracing feature which records detailed timing for every network operation.
Mix API and Browser Tests
Playwright's unique advantage is combining API calls with browser interactions in a single test. This hybrid approach uses the API for fast setup and teardown while using the browser to verify user-facing behavior. The result is tests that are significantly faster than pure browser tests but still validate the full user experience.
A typical hybrid pattern creates test data via API, then verifies it in the browser. The test calls request.post('/api/products', { data: productData }) to create a product, navigates the browser to the products page with page.goto('/products'), and asserts that the product appears in the UI with expect(page.locator('.product-card')).toContainText(productData.name). The API call takes 50ms where clicking through a creation form would take 5 seconds, but the browser verification still confirms the UI renders correctly.
Authentication is another prime candidate for the hybrid approach. Instead of filling out a login form through the browser (which involves page navigation, typing, clicking, and waiting for redirects), authenticate via API in the test's setup, extract the session cookie or token from the response, and inject it into the browser context. This gives each test an authenticated browser session in milliseconds rather than seconds, dramatically reducing test suite execution time.
Cleanup via API prevents test pollution. After a browser test creates data through the UI (testing the creation flow), the teardown phase deletes that data via API. This is faster and more reliable than navigating to a delete confirmation page through the browser, and it guarantees cleanup even if the browser test fails partway through.
Data verification roundtrips confirm full-stack correctness. Create data through the browser UI (testing the form submission), then verify it was stored correctly by reading it via API. Or modify data via API (simulating a background process or another user's action), then refresh the browser and verify the UI reflects the change. These cross-layer verifications catch bugs in the data flow between frontend and backend that same-layer tests miss.
Build Authentication Fixtures
Playwright fixtures are reusable setup functions that provide configured objects to tests. For API testing, authentication fixtures eliminate repeated login code and keep credentials management centralized.
The basic approach extends Playwright's test object with a custom fixture that creates an authenticated API context. Define a fixture function that sends a login request, extracts the token, and creates a new APIRequestContext with the token in the default headers. Tests that need authenticated API access declare this fixture as a parameter and receive a ready-to-use, authenticated request context.
For tests that need multiple user roles, create separate fixtures for each role: adminRequest, userRequest, guestRequest. Each fixture authenticates with different credentials and provides a context with the appropriate permissions. Tests that verify authorization logic use multiple fixtures in the same test, sending the same request through different contexts and asserting different status codes.
Storage state reuse is a Playwright-specific optimization for authentication. Instead of authenticating via API in every test, authenticate once in a setup project, save the authentication state (cookies, local storage) to a file, and load it in subsequent test projects. This means the login API call happens once per test run rather than once per test, saving significant time in large suites. The playwright.config.ts file defines a setup project that runs first and a main project that depends on it and loads the saved state.
Token refresh handling belongs in the fixture as well. If your API uses short-lived access tokens with refresh tokens, the fixture can check token expiration before each test and refresh automatically. This prevents mysterious 401 failures that happen when tests run long enough for the initial token to expire. The fixture encapsulates all this complexity so individual tests never think about authentication mechanics.
Run in CI/CD
Playwright's test runner generates results in multiple formats suitable for CI/CD integration. Configure reporters in playwright.config.ts: reporter: [['html', { open: 'never' }], ['junit', { outputFile: 'results.xml' }]]. The HTML reporter creates a browsable report with test results, request/response details, and failure screenshots. The JUnit reporter creates XML that CI tools like Jenkins, GitHub Actions, and GitLab CI parse natively.
For GitHub Actions, create a workflow file that installs dependencies, installs Playwright browsers (npx playwright install --with-deps), and runs tests (npx playwright test). Upload the HTML report as a build artifact for debugging failed tests. The JUnit XML report integrates with GitHub's test summary feature, showing pass/fail results directly in the pull request checks tab.
API-only tests do not need browser installation. If a test file only uses the request fixture and never opens a page, Playwright skips browser launch entirely, making API tests run as fast as any HTTP client. Tag API-only test files with a project configuration that disables browser downloads to speed up CI setup. Use Playwright's project feature to define separate configurations for API tests and browser tests, running API tests first (they are faster and catch most backend issues) and browser tests second.
Parallel execution is built into Playwright's test runner. By default, test files run in parallel across multiple workers. For API tests, this is safe as long as tests are isolated and do not share mutable state. Configure the number of workers in playwright.config.ts based on your API server's capacity: too many parallel workers can overwhelm the server and cause false failures from rate limiting or connection exhaustion. A good starting point is 4 workers for staging APIs and 1 worker for production synthetic tests.
Retry configuration handles transient API failures that are not genuine bugs. Set retries in playwright.config.ts (retries: 2 on CI) so a test that fails due to a brief network hiccup or database lock is retried before being reported as a failure. Playwright marks retried tests distinctly in reports, so you can identify flaky tests that pass on retry and investigate their root causes.
API Testing Patterns in Playwright
The page object model (POM) pattern that Playwright recommends for browser tests adapts well to API testing through API client classes. Create a class for each API resource (UserAPI, ProductAPI, OrderAPI) that encapsulates the request context and provides methods like create(), getById(), update(), and delete(). Each method sends the appropriate HTTP request, handles common response parsing, and returns typed objects. Tests call these methods instead of constructing raw HTTP requests, making them more readable and maintainable.
Response type definitions improve test quality in TypeScript projects. Define interfaces for each API response shape (UserResponse, ProductListResponse, ErrorResponse) and cast the parsed JSON to these types. TypeScript then catches assertions on wrong field names, incorrect property chains, and type mismatches at compile time rather than at test runtime. This eliminates an entire class of test bugs where a test passes because a typo in a field name assertion quietly tests an undefined value.
Shared validation functions reduce duplication across tests. If every test that creates a user needs to verify the same response structure, extract that validation into a helper function. Create functions like expectValidUserResponse(response) that check status code, required fields, data types, and field value constraints. Tests call these shared validators after their specific assertions, ensuring consistent quality checks without repeating boilerplate code.
Test tagging with Playwright's annotation system lets you categorize API tests by type. Tag smoke tests that should run on every commit, regression tests that run nightly, performance tests that run weekly, and security tests that run before releases. Use the --grep flag to run specific categories: npx playwright test --grep @smoke runs only smoke tests. This flexibility lets you build a single test suite that serves multiple testing purposes with different CI triggers.
Playwright's API testing capability eliminates the need for a separate API testing tool for teams already using Playwright for browser automation. The APIRequestContext provides full HTTP client functionality with the same test runner, assertion library, and reporting you already know. The real power is hybrid tests that mix API calls for fast setup and teardown with browser interactions for UI verification, giving you the speed of API testing with the confidence of browser testing.