Playwright's request fixture makes HTTP calls without opening a browser, completing in 50–200 milliseconds versus 3–5 seconds for a UI test. It's available alongside page in any Playwright test, which enables a hybrid pattern: create test data via API, verify it through the UI, and clean up via API, all in a single test file without any extra tooling. This article covers GET, POST, authentication, error response testing, and the combined API-plus-UI patterns that eliminate flaky setup.

Why API tests belong in your Playwright suite

The case for API testing isn't about replacing UI tests. It's about testing the right things at the right level.

A UI test that creates a travel item goes through the browser, renders the form, fills inputs, clicks submit, waits for the page to update, then checks the result. It takes 3–5 seconds and can fail for a dozen reasons unrelated to the actual feature: a slow animation, a changed locator, a flaky network request.

An API test that creates a travel item sends one HTTP request and checks the response. It takes 50–200 milliseconds and fails only when the API itself is broken.

Use API tests for: data validation, authentication rules, error responses, business logic in the backend. Use UI tests for: user flows, visual rendering, front-end interactions. Both have a place. The mistake is using only UI tests when the bug is in the API.

The request fixture

Playwright exposes an APIRequestContext through the request fixture. It's available in any test the same way page is:

import { test, expect } from '@playwright/test';

test('GET /api/items returns a list', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');

  expect(response.status()).toBe(200);

  const items = await response.json();
  expect(items.length).toBeGreaterThan(0);
});

No browser opens. No page loads. Just an HTTP request and a response. The request fixture handles cookies, headers, and base URL configuration automatically.

GET requests and response validation

The most common API test pattern: call an endpoint, check the status, check the shape of the data.

test('GET /api/items returns correct structure', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');

  // Check HTTP status
  expect(response.status()).toBe(200);
  expect(response.ok()).toBeTruthy(); // true for 2xx status codes

  // Parse the response body
  const items = await response.json();

  // Check the array
  expect(Array.isArray(items)).toBe(true);
  expect(items.length).toBeGreaterThan(0);

  // Check the shape of the first item
  const firstItem = items[0];
  expect(firstItem).toHaveProperty('id');
  expect(firstItem).toHaveProperty('destination');
  expect(firstItem).toHaveProperty('status');
});

response.ok() is a shorthand for status >= 200 && status < 300. Use it when you only care that the request succeeded.

To check response headers:

const contentType = response.headers()['content-type'];
expect(contentType).toContain('application/json');

POST requests

POST requests send data to create a resource. Pass the body as JSON:

test('POST /api/items creates a new item', async ({ request }) => {
  const response = await request.post('https://lab.becomeqa.com/api/items', {
    data: {
      destination: 'Tokyo',
      status: 'planned',
      notes: 'Cherry blossom season'
    }
  });

  expect(response.status()).toBe(201);

  const created = await response.json();
  expect(created).toHaveProperty('id');
  expect(created.destination).toBe('Tokyo');
  expect(created.status).toBe('planned');
});

The data option automatically serializes to JSON and sets the Content-Type: application/json header.

For form-encoded requests use form instead of data:

const response = await request.post('/api/login', {
  form: {
    username: 'admin@becomeqa.com',
    password: 'testpass123'
  }
});

Authentication

Most real APIs require authentication. There are two common patterns.

Bearer token in header:

test('authenticated GET returns user data', async ({ request }) => {
  // First, get a token
  const loginResponse = await request.post('https://lab.becomeqa.com/api/auth/login', {
    data: {
      username: 'admin@becomeqa.com',
      password: 'testpass123'
    }
  });

  const { token } = await loginResponse.json();

  // Use the token in subsequent requests
  const response = await request.get('https://lab.becomeqa.com/api/items', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  expect(response.status()).toBe(200);
});

Cookie-based auth via UI login:

If your app uses cookies for auth, you can log in through the browser and reuse the session for API calls. The page.request object shares cookies with the browser context:

test('API call with browser session', async ({ page, request }) => {
  // Log in through the UI
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  // Now use page.request — it carries the auth cookie
  const response = await page.request.get('https://lab.becomeqa.com/api/items');
  expect(response.status()).toBe(200);
});

Note the difference: request (the fixture) is a standalone context with no browser cookies. page.request shares cookies with the current browser session.

Testing error responses

APIs should return meaningful errors. Test them explicitly.

test('GET /api/items/:id returns 404 for unknown id', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items/nonexistent-id-99999');

  expect(response.status()).toBe(404);

  const body = await response.json();
  expect(body).toHaveProperty('error');
});

test('POST /api/items returns 400 for missing required fields', async ({ request }) => {
  const response = await request.post('https://lab.becomeqa.com/api/items', {
    data: {
      // missing required 'destination' field
      status: 'planned'
    }
  });

  expect(response.status()).toBe(400);
});

test('protected endpoint returns 401 without auth', async ({ request }) => {
  const response = await request.get('https://lab.becomeqa.com/api/items');
  // no Authorization header
  expect(response.status()).toBe(401);
});

These tests verify that the API handles bad input correctly, not just the happy path.

Combining API and UI tests

One of the most powerful patterns: use the API to set up test data, then verify it through the UI. Or the reverse: interact through the UI, verify through the API.

API setup, UI verification:

test('created item appears in the UI table', async ({ page, request }) => {
  // Create item via API — fast and reliable
  const createResponse = await request.post('https://lab.becomeqa.com/api/items', {
    data: { destination: 'Lisbon', status: 'planned' }
  });
  const { id } = await createResponse.json();

  // Verify it shows up in the UI
  await page.goto('https://lab.becomeqa.com');
  // ... login steps ...
  await expect(page.getByText('Lisbon')).toBeVisible();

  // Cleanup via API after the test
  await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});

This test is faster and more reliable. UI setup is the most common source of flakiness in tests that aren't actually testing the UI.

UI action, API verification:

test('deleting an item via UI removes it from the API', async ({ page, request }) => {
  // Setup via API
  const createResponse = await request.post('https://lab.becomeqa.com/api/items', {
    data: { destination: 'Oslo', status: 'planned' }
  });
  const { id } = await createResponse.json();

  // Action via UI
  await page.goto('https://lab.becomeqa.com');
  // ... login, find item, click delete ...

  // Verify via API
  const checkResponse = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
  expect(checkResponse.status()).toBe(404);
});

Configure a base URL for API tests

Repeating the full URL in every test is noisy. Set baseURL in playwright.config.ts:

export default defineConfig({
  use: {
    baseURL: 'https://lab.becomeqa.com',
  },
});

Now you can use relative paths:

const response = await request.get('/api/items');
const response = await request.post('/api/items', { data: { ... } });

For projects that have separate base URLs for UI and API, create a custom fixture that configures the API context separately.

Add extraHTTPHeaders to the config if your API requires a common header on every request (like an API key or a custom X-App-Version header). Set it once in the config rather than in every test.

export default defineConfig({
  use: {
    baseURL: 'https://lab.becomeqa.com',
    extraHTTPHeaders: {
      'X-Api-Key': process.env.API_KEY || '',
    },
  },
});

Organize API tests separately

Keep API tests in their own folder so you can run them independently from UI tests:

tests/
  ui/
    login.spec.ts
    items.spec.ts
  api/
    items-api.spec.ts
    auth-api.spec.ts

# Run only API tests — fast, no browser needed
npx playwright test tests/api/

# Run only UI tests
npx playwright test tests/ui/

API tests run significantly faster than UI tests. Running them separately in CI lets you get fast feedback on backend bugs before the slower UI suite finishes.

FAQ

Do I still need Postman if I use Playwright for API testing?

Postman is useful for manual API exploration: figuring out what endpoints exist, what parameters they accept, what responses look like. Once you know what you're testing, write the automated version in Playwright. Both have their place.

Should every API endpoint have a test?

Focus on the endpoints that matter: authentication, core data operations, and any endpoint with complex validation logic. A CRUD API for travel items needs tests for create, read, update, delete, and at least the main error cases (404, 400, 401). Not every combination of inputs.

How do I test file uploads via API?

Use the multipart option:

const response = await request.post('/api/upload', {
  multipart: {
    file: {
      name: 'test.pdf',
      mimeType: 'application/pdf',
      buffer: Buffer.from('fake pdf content'),
    }
  }
});

Can I use the request fixture without a page?

Yes. Tests that only use request and not page run without opening a browser at all. They're pure HTTP tests and run as fast as any other HTTP client.

→ See also: API Testing 101: What Every QA Engineer Needs to Know in 2026 | API Testing with Playwright's APIRequestContext (No Postman Required) | Authentication in API Tests: API Keys, Bearer Tokens, OAuth2, JWT | Advanced API Testing with Playwright: Patterns for Real Projects