API Testing & Request Context — The Speed Hack
UI tests are thorough but slow. API tests are fast but lack rendering context. Playwright lets you blend both. Learn to use APIRequestContext to set up test data in milliseconds and test backends without a browser.
📖 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)
🟢 API Setup (Fast)
request.post('/api/orders')🚀 Pure API Tests (No Browser)
You can write tests that never open a browser. This is perfect for backend regression testing.
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.
// 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.
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
// ❌ 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
// ❌ 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
- Use API for setup and teardown. Create records via API, test the UI, then delete records via API.
- Don't mix API logic with UI assertions unnecessarily. If a test is purely API, don't request the
pagefixture. - Use
request.newContext()in custom fixtures if you need isolated API sessions per test. - Set base URLs in config. Use
baseURLinplaywright.config.tsso you can writerequest.get('/users')instead of the full URL. - 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
Show Answer
request fixture: await request.post('/api/endpoint', { data: { key: 'value' } }).Show Answer
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.Show Answer
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.Show Answer
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
API Setup, UI Assert, API Teardown
- Find a public test API (e.g., Todoist API, Reqres, or your internal app).
- Write a Playwright test that uses
request.post()to create a new task/item. - Extract the ID from the API response.
- Use the
pagefixture to navigate to the UI page for that specific item. - Assert via UI that the item exists.
- Use
request.delete()in anafterEachhook 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).
- Test 1 (Auth): POST to
/login, assert 200, assert token exists. - Test 2 (Create): POST to
/productswith a new product. Assert 201. - Test 3 (Read): GET
/products/{id}. Assert the name matches what was created. - Test 4 (Update): PUT to
/products/{id}with a new price. Assert 200. - 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.
pagebrowserrequestapirequest fixture exposes the APIRequestContext for making HTTP calls.request fixture and not page, what happens?page fixture is not requested.response.body()response.text()response.parse()await response.json()await response.json() reads the stream and parses the JSON payload into an object.?role=admin in a GET request?params option: request.get('/users', { params: { role: 'admin' } })query optiondata optionparams object automatically serializes into URL query parameters.page.requestAPIRequestContext operates in Node.js, it is completely immune to CORS.page.setToken(token)page.addCookies() or page.setExtraHTTPHeaders()request.setHeader()Authorization header for all browser requests.baseURL for your API tests?playwright.config.ts under usebaseURL in the config allows you to use relative paths like /api/users in your tests.afterEach or afterAll hooks to call request.delete()❓ 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
requestfixture 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
afterEachto call DELETE endpoints and clean up test data.
