Testing APIs

Use the `request` fixture to hit HTTP endpoints directly — no browser, no DOM.

Playwright is famous for driving browsers, but it ships an HTTP client, too. The request fixture makes plain HTTP calls and returns an APIResponse you can assert on — no page, no rendering, no waiting for hydration.

Reach for it when:

  • The thing you want to verify lives in JSON, not in pixels (status codes, error shapes, response bodies).
  • You want to seed state cheaply before a UI test (create a user, fill a cart, place an order) instead of clicking through the flow.
  • You need to test an endpoint that has no UI yet.

request vs page.route()

You met page.route() in the network lesson — that one intercepts calls the browser is about to make. request is the opposite: your test makes the call itself, no browser involved.

test("hits the products endpoint", async ({ request }) => {
  const response = await request.get("/api/products/");

  await expect(response).toBeOK();

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

A few details worth pinning down:

  • baseURL from playwright.config still applies.
  • expect(response).toBeOK() is the idiomatic assertion. It passes for any 2xx status and is much friendlier than expect(response.ok()).toBeTruthy().
  • await response.json() reads the body. There's also .text() and .body() if you want raw bytes.

request vs page.request

Same HTTP client, two cookie jars. The choice between them comes down to a single question: does this call need to share state with the browser?

  • request (the test-level fixture) launches no browser and keeps its own storage. Reach for it in standalone API suites — endpoint shape, status codes, JSON schemas — where the response doesn't depend on a logged-in user.
  • page.request is bound to the page's BrowserContext. Anything the browser logged in to, anything page.goto set as a cookie, comes along for the ride. Reach for it whenever an API call has to see or set cookies the page also touches.
// Standalone API test — no browser, separate cookie jar
test("products endpoint", async ({ request }) => {
  const response = await request.get("/api/products/");
  await expect(response).toBeOK();
});

// Mixed flow — log in via UI, then hit the API with shared cookies
test("authenticated checkout", async ({ page }) => {
  await page.goto("/login");
  // … make a request with all existing cookies
  const response = await page.request.post("/api/checkout/", { data: payload });
  await expect(response).toBeOK();
});

Sending data and headers

post, put, patch, and delete all take an options bag with data (JSON), form (URL-encoded), multipart (file uploads), and headers.

const response = await request.post("/api/checkout/", {
  data: {
    email: "test@example.com",
    /* …rest of the payload */
  },
  headers: { "x-test": "playwright" },
});

For headers you want on every request in the test run, set extraHTTPHeaders in playwright.config instead of repeating yourself.

Inline exercise

Assert the products response shape

/api/products/ returns an array of Shopify products. Each product has a priceRange.minVariantPrice with an amount and a currencyCode.

  1. Use request.get("/api/products/") to hit the endpoint.
  2. Assert the response is OK.
  3. Assert every product has a non-empty priceRange.minVariantPrice.amount.
Show solution
test("every product has a price", async ({ request }) => {
  const response = await request.get("/api/products/");
  await expect(response).toBeOK();

  const products = await response.json();
  for (const product of products) {
    expect(Number(product.priceRange.minVariantPrice.amount)).toBeGreaterThan(0);
  }
});

Where API tests live

Two common patterns:

  • Same file as UI tests when you mix them in one scenario (seed via request, then verify in the browser).
  • Dedicated *.api.spec.ts files in their own project when the suite is API-only — that project can skip browser launch for a real speed win.
Note

The Playwright API testing guide covers authentication, request context reuse, and mocking the API layer for UI tests. Worth a read once you've shipped your first few.


Hands on

Practice testing APIs

Exercise 1 of 3

Probe the checkout endpoint without logging in

POST /api/checkout/ requires a name cookie. Without one, it returns 401 Unauthorized — a tiny probe that fits the request fixture perfectly: no browser, no DOM, no setup.

  1. POST with no body.
  2. Assert response.status() is 401.
Exercise 2 of 3

Log in via API, see your name in the navbar

POST /api/login/ accepts { name } and sets a name cookie. Skip the login form entirely: hit the endpoint, then navigate and watch the page render as if you'd already signed in.

Use page.request so the cookie lands on the page's context.

  1. Make the request and assert the response is OK.
  2. page.goto("/") and assert the welcome banner show the right name.
Exercise 3 of 3

Speed up your set up steps

With the knowledge about /api/login you can now also speed up all your *.setup.ts files. Making an API call instead of spinning up a browser will save your tests some time and resources.