📖 The Story: The 5-Hour Setup

Elena was an SDET at a streaming company. She needed to write a test that verified a user could see their "Watch History" on the dashboard. The catch? To have watch history, the user needed to watch 5 different videos for at least 30 seconds each via the web player.

Her first attempt was pure UI. The test logged in, searched for Video 1, clicked play, waited 30 seconds, paused it, went back, searched for Video 2... The test took 4 minutes to set up the data and 2 seconds to assert the UI. With 20 similar tests, her suite took 5 hours to run.

Elena realized she didn't need to test the video player; she just needed the watch history to exist. She used Playwright's request.post() to call the backend API directly, injecting 5 watch events in 500 milliseconds. The 4-minute setup vanished. The suite dropped from 5 hours to 12 minutes.

If you are using the UI to prepare data for a test, you are testing the wrong thing. Use APIs for setup; use UI for verification.

🎯 Why API Testing?

Lightning Setup

Create users, orders, and data via API in milliseconds instead of clicking through forms.

🧹

Clean Teardown

Delete test data via DELETE requests after the test, leaving the database pristine.

🔌

No Browser Needed

Pure API tests run without a browser instance, saving massive CI resources.

🔗

Shared State

Log in via API, pass the cookies to the browser, and skip the UI login page entirely.

🌍 Real World Example

You need to test the "Delete Order" button on the UI. To test this, an order must exist. If you use the UI, you must navigate to products, add to cart, go to checkout, fill shipping, fill payment, and place the order. That's 15 UI actions.

Instead, you use await request.post('/api/orders', { data: {...} }). The order is created instantly. Now your UI test only focuses on clicking the "Delete" button and verifying it's gone. The setup is isolated; the test is pure.

🧒 Explain Like I'm 10

Imagine you want to test if a library's "return book" machine works. To test it, you need a checked-out book.

The UI Way: You walk into the library, find a book, take it to the front desk, show your library card, wait for the librarian to check it out, and then walk over to the return machine.

The API Way: You sneak into the library's back office, stamp the book as "checked out" in 1 second, and walk straight to the return machine.

You aren't testing the checkout process; you just need the book to be checked out so you can test the return machine. APIs are the back office.

🎓 Professional Explanation

Playwright provides the APIRequestContext class for making HTTP requests. It is exposed in tests via the built-in request fixture. Unlike browser-based requests, these originate from Node.js, bypassing CORS restrictions and browser rendering overhead.

Because it runs in Node.js, it shares the same test runner lifecycle. You can mix API calls and UI interactions in the exact same test. Furthermore, APIRequestContext can persist cookies and headers, allowing you to authenticate via API and seamlessly transfer that session to a BrowserContext.

📊 UI vs API Setup

Setting up an "Order" for a UI Test

🔴 UI Setup (Slow)
1Navigate to Products
2Click Add to Cart
3Click Checkout
4Fill Shipping Form
5Fill Payment Form
6Click Place Order
7Wait for Success Page
🟢 API Setup (Fast)
1request.post('/api/orders')
Order created. Ready to test.

🚀 Pure API Tests (No Browser)

You can write tests that never open a browser. This is perfect for backend regression testing.

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

// Note: we do NOT request the 'page' fixture
test('get user by id', async ({ request }) => {
  const response = await request.get('/api/users/1');
  
  expect(response.status()).toBe(200);
  
  const user = await response.json();
  expect(user.id).toBe(1);
  expect(user.name).toBeTruthy();
});

Because the page fixture is not requested, Playwright will not launch a browser (like Chromium) for this test, saving memory and execution time.

📡 HTTP Methods

Playwright supports all standard HTTP methods with a clean, Promise-based API.

typescript
// GET request
await request.get('/api/users');

// POST request with JSON body
await request.post('/api/users', {
  data: { name: 'Alice', email: 'alice@test.com' }
});

// PUT request (update)
await request.put('/api/users/1', {
  data: { name: 'Alice Smith' }
});

// DELETE request
await request.delete('/api/users/1');

// Passing query parameters
await request.get('/api/users', {
  params: { role: 'admin', active: 'true' }
});

🔐 Sharing Auth State (The Holy Grail)

The most powerful use case: log in via API, then use that session for UI tests. This skips the slow UI login form on every test.

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

test('view dashboard after API login', async ({ page, request }) => {
  // 1. Login via API
  const apiResponse = await request.post('/api/login', {
    data: { username: 'admin', password: 'password' }
  });
  
  // 2. Get the auth token / cookies from the API response
  const { token } = await apiResponse.json();

  // 3. Inject the token into the browser context
  await page.setExtraHTTPHeaders({
    'Authorization': `Bearer ${token}`
  });
  
  // OR if using cookies:
  await page.context().addCookies([{
    name: 'session',
    value: token,
    url: 'https://app.com'
  }]);

  // 4. Navigate directly to the protected page (No UI login!)
  await page.goto('/dashboard');
  await expect(page.getByRole('heading', { name: 'Welcome Admin' })).toBeVisible();
});

This pattern is so common that Playwright has a built-in feature for it: storageState. You can authenticate once in a global setup script, save the cookies/localStorage to a JSON file, and pass that file to all your UI tests.

⚠️ Common Mistakes

Mistake 1: Using UI for Test Setup

typescript · ❌ Slow
// ❌ BAD: Using UI actions to create a user before testing delete
await page.goto('/register');
await page.fill('#name', 'Test User');
await page.fill('#email', 'test@test.com');
// ... 5 more UI steps
await page.click('Delete User');

// ✅ GOOD: Use API to create the user instantly
await request.post('/api/users', { data: { name: 'Test User' } });
await page.goto('/users');
await page.click('Delete User');

Mistake 2: Forgetting to Parse JSON

typescript · ❌ Unparsable
// ❌ BAD: response.body() is a readable stream, not an object
const resp = await request.get('/api/users/1');
console.log(resp.body().name); // undefined!

// ✅ GOOD: await response.json() to parse the body
const user = await resp.json();
console.log(user.name); // "Alice"

✅ Best Practices

  1. Use API for setup and teardown. Create records via API, test the UI, then delete records via API.
  2. Don't mix API logic with UI assertions unnecessarily. If a test is purely API, don't request the page fixture.
  3. Use request.newContext() in custom fixtures if you need isolated API sessions per test.
  4. Set base URLs in config. Use baseURL in playwright.config.ts so you can write request.get('/users') instead of the full URL.
  5. Handle auth tokens securely. Don't hardcode passwords in test files; use environment variables.

🏗️ Senior Engineer Deep Dive

How APIRequestContext is Implemented

Unlike browser fetch which is subject to CORS and browser security policies, APIRequestContext uses Node.js's native HTTP modules (specifically undici under the hood in modern Playwright). This makes it incredibly fast and immune to CORS errors that plague browser-based API testing tools.

Network Prioritization

When you mix UI and API tests, the UI browser context and the Node API context share network resources. However, API calls from Node.js are prioritized and don't block the browser's event loop. This allows test setup to happen asynchronously while the browser is still rendering the previous page.

Storage State Architecture

When you authenticate via API and save a storageState file, Playwright writes a JSON containing cookies and origins (localStorage). When a browser context starts, it reads this file and hydrates the browser state before any page loads. This is architecturally identical to how Service Workers cache session data, allowing instant login restoration.

💼 Interview Questions

Beginner
How do you make a POST request in a Playwright test?
Show Answer
Use the built-in request fixture: await request.post('/api/endpoint', { data: { key: 'value' } }).
Intermediate
How can you run a Playwright test without launching a browser?
Show Answer
Only request the request fixture in your test function, omitting the page fixture. Playwright detects that no browser is needed and runs the test purely in Node.js.
Advanced
Explain how you would share authentication state between an API login and a UI test.
Show Answer
I would perform the login via request.post('/api/login'), extract the token or cookies from the response, and use page.setExtraHTTPHeaders() or context.addCookies() to inject them into the browser. Alternatively, save the state to a file using storageState and pass it to the browser context.
Scenario
You need to test a complex checkout flow, but creating an order via the UI takes 2 minutes. How do you optimize?
Show Answer
Use request.post('/api/orders') in a beforeAll hook to create the order instantly. Store the order ID. In the test, navigate directly to the order details page using that ID and perform the UI assertion. After the test, use request.delete('/api/orders/{id}') to clean up.

🏋️ Practical Exercise

🎯 Hands-On Practice Medium

API Setup, UI Assert, API Teardown

  1. Find a public test API (e.g., Todoist API, Reqres, or your internal app).
  2. Write a Playwright test that uses request.post() to create a new task/item.
  3. Extract the ID from the API response.
  4. Use the page fixture to navigate to the UI page for that specific item.
  5. Assert via UI that the item exists.
  6. Use request.delete() in an afterEach hook to delete the item.

🚀 Mini Project

🏗️ The Headless E-Commerce Engine

Write a complete API test suite for an e-commerce backend (use a mock server or real API).

  1. Test 1 (Auth): POST to /login, assert 200, assert token exists.
  2. Test 2 (Create): POST to /products with a new product. Assert 201.
  3. Test 3 (Read): GET /products/{id}. Assert the name matches what was created.
  4. Test 4 (Update): PUT to /products/{id} with a new price. Assert 200.
  5. Test 5 (Delete): DELETE /products/{id}. Assert 204. Then GET the same ID and assert 404.

Run the suite. Notice how 5 tests run in under 2 seconds because no browser is launched.

📝 Quiz

Test your understanding. Click an option to check your answer.

1. Which fixture is used to make API requests in a Playwright test?
A) page
B) browser
C) request
D) api
Answer: C — The built-in request fixture exposes the APIRequestContext for making HTTP calls.
2. If you only request the request fixture and not page, what happens?
A) The test fails
B) Playwright runs the test without launching a browser
C) Playwright launches a headless browser anyway
D) The test runs slower
Answer: B — Playwright optimizes resources by not launching a browser instance if the page fixture is not requested.
3. How do you parse the body of an API response into a JavaScript object?
A) response.body()
B) response.text()
C) response.parse()
D) await response.json()
Answer: D — await response.json() reads the stream and parses the JSON payload into an object.
4. Why is using the API for test setup preferred over UI setup?
A) It tests the backend logic better
B) It is significantly faster and less flaky
C) It uses less memory
D) It bypasses CORS
Answer: B — API setup avoids multiple UI rendering steps and network latency, making tests exponentially faster.
5. How do you pass query parameters like ?role=admin in a GET request?
A) Append them to the URL string directly
B) Use the params option: request.get('/users', { params: { role: 'admin' } })
C) Use the query option
D> Use the data option
Answer: B — The params object automatically serializes into URL query parameters.
6. Can API tests in Playwright be affected by CORS errors?
A) Yes, because it runs in a browser
B) No, because requests are made from Node.js, not the browser
C) Only if using page.request
D) Only when testing external APIs
Answer: B — CORS is a browser security mechanism. Since APIRequestContext operates in Node.js, it is completely immune to CORS.
7. How can you inject an auth token obtained via API into a UI test?
A) page.setToken(token)
B) page.addCookies() or page.setExtraHTTPHeaders()
C) request.setHeader()
D) You cannot; you must log in via UI
Answer: B — You can add cookies to the context or set the Authorization header for all browser requests.
8. What HTTP method is typically used to create a new resource?
A) GET
B) POST
C) PUT
D) DELETE
Answer: B — POST is standard for creating new resources.
9. Where should you configure the baseURL for your API tests?
A) In the test file
B) In playwright.config.ts under use
C) In a JSON file
D) Environment variables only
Answer: B — Setting baseURL in the config allows you to use relative paths like /api/users in your tests.
10. What is the best way to clean up test data created via API?
A) Restart the database server
B) Use afterEach or afterAll hooks to call request.delete()
C) Write a UI script to delete it
D) Leave it; it doesn't affect tests
Answer: B — Using hooks to call DELETE endpoints ensures the database remains clean for the next test run.

❓ FAQ

Q: Can I use Playwright to test GraphQL APIs?

A: Yes. GraphQL operates over standard HTTP POST requests. You use request.post('/graphql', { data: { query: '...' } }).

Q: Does APIRequestContext support file uploads?

A: Yes, you can upload files using the multipart option in a POST request, passing a buffer or file path.

Q: How do I handle cookies automatically in API tests?

A: The request fixture maintains a cookie jar per test by default, meaning login cookies are automatically sent with subsequent requests.

📦 Summary

🎯 Key Takeaways

  • Use the request fixture for HTTP calls without needing a browser.
  • API setup is 100x faster than UI setup. Use it to create test data.
  • Parse responses using await response.json().
  • Share auth state by passing tokens/cookies from the API to the BrowserContext.
  • Use afterEach to call DELETE endpoints and clean up test data.