IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Intermediate·16 / 27

API testing

Playwright can make HTTP requests directly from the test — no browser needed. Useful for setting up test data, calling REST APIs, and checking server-side state.

Why call the API from tests

Use the request fixture to seed data via API (fast), then use the page fixture to verify it in the UI

Browser tests are slow for setup. If your test needs 10 orders in the database before it runs, creating them through the UI takes 10 form submissions. Creating them via API takes 10 HTTP calls — 10-50x faster. I use API calls for three things in my tests:

  1. Seed test data before the UI test starts
  2. Call the API directly and verify the JSON response
  3. Check server state after a UI action (did the record actually get saved?)

The request fixture

Playwright Test gives you a request fixture in every test — it's an APIRequestContext that can make GET, POST, PUT, DELETE and other HTTP requests. It shares cookies with the test's browser context, so if you're logged in via the UI, the API calls are also authenticated.

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

test('GET /api/orders returns list', async ({ request }) => {
  const response = await request.get('/api/orders')

  expect(response.status()).toBe(200)
  const body = await response.json()
  expect(body.orders).toBeInstanceOf(Array)
  expect(body.total).toBeGreaterThanOrEqual(0)
})

test('POST /api/orders creates an order', async ({ request }) => {
  const response = await request.post('/api/orders', {
    data: {
      item: 'Laptop',
      quantity: 1,
      customerId: 'cust-001',
    },
  })

  expect(response.status()).toBe(201)
  const order = await response.json()
  expect(order.id).toBeTruthy()
  expect(order.status).toBe('pending')
})

Seed data before UI tests

The most practical use: create test data via API, then verify it in the UI. This avoids clicking through forms just to set up a starting state. The UI test focuses on what it's actually testing.

ts
test('order appears in dashboard after creation', async ({ request, page }) => {
  // Створюємо замовлення через API (швидко, без форм)
  const res = await request.post('/api/orders', {
    data: { item: 'Keyboard', quantity: 2 },
  })
  const { id: orderId } = await res.json()

  // Перевіряємо в UI що воно з'явилось
  await page.goto('/dashboard')
  await expect(page.getByRole('row').filter({ hasText: orderId })).toBeVisible()
})

test('deleting from UI removes from API', async ({ request, page }) => {
  // Seed через API
  const res = await request.post('/api/orders', {
    data: { item: 'Mouse', quantity: 1 },
  })
  const { id: orderId } = await res.json()

  // Видаляємо через UI
  await page.goto('/orders')
  await page.getByRole('row').filter({ hasText: orderId })
    .getByRole('button', { name: 'Delete' }).click()
  await page.getByRole('button', { name: 'Confirm' }).click()

  // Перевіряємо що API повертає 404
  const checkRes = await request.get(`/api/orders/${orderId}`)
  expect(checkRes.status()).toBe(404)
})

Configure base URL

Set baseURL in the config and you can use relative paths in all requests — both in page.goto() and in request.get(). This makes it easy to switch between local, staging, and production environments.

ts
// playwright.config.ts
export default defineConfig({
  use: {
    baseURL: 'https://app.example.com',
  },
})

// В тесті — відносні шляхи
test('api and ui share base url', async ({ request, page }) => {
  await page.goto('/orders')         // → https://app.example.com/orders
  const res = await request.get('/api/orders')  // → https://app.example.com/api/orders
})

API context without a browser

For pure API tests — no browser needed at all — use request.newContext() to create a standalone APIRequestContext. This is faster and uses fewer resources than launching a full browser context.

ts
import { test, expect, request } from '@playwright/test'

test('create and fetch order via API only', async () => {
  const apiCtx = await request.newContext({
    baseURL: 'https://app.example.com',
    extraHTTPHeaders: {
      'Authorization': `Bearer ${process.env.API_TOKEN}`,
    },
  })

  const createRes = await apiCtx.post('/api/orders', {
    data: { item: 'Monitor', quantity: 1 },
  })
  expect(createRes.status()).toBe(201)

  const { id } = await createRes.json()
  const getRes = await apiCtx.get(`/api/orders/${id}`)
  expect(getRes.status()).toBe(200)

  await apiCtx.dispose()
})