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:
baseURLfromplaywright.configstill applies.expect(response).toBeOK()is the idiomatic assertion. It passes for any 2xx status and is much friendlier thanexpect(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.requestis bound to the page'sBrowserContext. Anything the browser logged in to, anythingpage.gotoset 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.
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.
- Use
request.get("/api/products/")to hit the endpoint. - Assert the response is OK.
- 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.tsfiles in their own project when the suite is API-only — that project can skip browser launch for a real speed win.
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.
Practice testing APIs
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.
POSTwith no body.- Assert
response.status()is401.
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.
- Make the request and assert the response is OK.
page.goto("/")and assert the welcome banner show the right name.
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.