Testing APIs

Use the `request` fixture to hit HTTP endpoints directly without a browser.

Playwright is known for browser-based end-to-end testing, but it ships with an HTTP client, too. The request object makes plain HTTP calls and returns an APIResponse you can assert on. It works without a page or browser context.

Use it when:

  • The thing you want to verify lives in API-land (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. It intercepts calls the browser is about to make. request is the opposite. Your test makes the call itself without a browser context.

request provides methods for the common HTTP operations: get, head, post, put, delete, etc.

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);});

There are some things to note:

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

request vs page.request

The request object isn't the only way to make HTTP calls. There's also page.request. What's the difference?

Both request objects use the same HTTP client but with a different context. To pick one, ask yourself: does this call need to share state with the browser?

  • request (the test-level fixture) launches no browser and keeps its own storage. Use it in standalone API suites where the response doesn't depend on a logged-in user.
  • page.request is bound to the page's BrowserContext. Every session the browser logged in to and every cookie page.goto set comes along. Use it whenever an API call has to see or set cookies the page also touches.
// Standalone API test: no browser, separate session contexttest("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 cookiestest("authenticated checkout", async ({ page }) => {  await page.goto("/login");  // … make a request with the current browser state and all existing cookies  const response = await page.request.post("/api/checkout/", {    /* ... */  });  await expect(response).toBeOK();});

Sending data and headers

The post, put, patch, and delete methods accept an options object 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" },
});
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);
  }
});

But where should you put your API tests?

There are two common patterns:

Mixed scenarios
same file as UI tests

Seed state via request, then verify the result in the browser. API call and UI check tell one story, so they live together.

Seeding state
create a user or fill a cart without clicking through the flow
Shared sessions
page.request carries the page's cookies along
One scenario, one file.
API-only suites
dedicated API spec files

Name them *.api.spec.ts and give them their own project. That project can skip the browser launch for a real speed win.

Contract checks
status codes, error shapes, response bodies
Endpoints without a UI
nothing to click, but the API is testable
No browser, no DOM, no waiting.

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. That's a quick test and a perfect fit for request because it needs no browser and no setup.

  1. POST to the checkout 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 from within the browser context and assert the response is OK.
  2. Use page.goto("/") and assert the welcome banner shows the right name.
Exercise 3 of 3

Speed up your setup steps

Now that you know about /api/login/, you can speed up all your *.setup.ts files. Making an API call instead of spinning up a browser saves your tests time and resources.