QA How-To
How to Use Playwright APIRequestContext (2026)
Learn Playwright APIRequestContext in 2026 with setup, authentication, cookies, JSON, uploads, negative tests, cleanup, and runnable TypeScript examples.
19 min read | 2,406 words
TL;DR
Use the built-in `request` fixture for most Playwright API tests. Configure `baseURL` and common headers, assert `APIResponse` status and payload, and dispose any context you create with `request.newContext()`.
Key Takeaways
- Use the Playwright Test request fixture for ordinary isolated API tests and setup.
- Create and dispose a standalone request context when identity, headers, storage, or lifecycle must be custom.
- Use browserContext.request only when API calls should intentionally share browser cookies.
- Assert exact status, content type, runtime payload shape, and business invariants instead of only checking success.
- Keep negative responses inspectable by leaving failOnStatusCode disabled for contract tests.
- Treat request timeout, redirect, retry, test data, and cleanup rules as part of API test design.
playwright APIRequestContext is Playwright's HTTP client for API tests, test-data setup, authentication, and backend verification without driving the browser UI. In Playwright Test, use the built-in request fixture for an isolated request context, or create one with playwright.request.newContext() when you need custom lifecycle and configuration.
The important design choice is context ownership. An isolated context manages its own cookies and must be disposed when you create it. A context obtained from browserContext.request shares cookie storage with that browser context, which makes API login and browser setup work together. This guide explains both models using current TypeScript APIs for 2026.
TL;DR
import { test, expect } from '@playwright/test';
test('gets the current user through the API', async ({ request }) => {
const response = await request.get('/api/me');
await expect(response).toBeOK();
const user = await response.json() as { id: string; email: string };
expect(user.email).toBe('qa@example.com');
});
| Context source | Cookie relationship | Lifecycle | Best use |
|---|---|---|---|
Test request fixture |
Isolated for the test | Runner-managed | API tests and data setup |
playwright.request.newContext() |
Isolated | You call dispose() |
Custom clients and non-test utilities |
browserContext.request |
Shares browser cookies | Browser context owns it | API login plus UI flow |
Set baseURL and common headers centrally, assert both transport and payload, and remember that non-2xx responses are returned by default unless failOnStatusCode is enabled.
1. What playwright APIRequestContext Is
APIRequestContext sends HTTP and HTTPS requests and returns an APIResponse. It supports convenience methods for get, post, put, patch, delete, and head, plus the general fetch method. Request options cover JSON data, HTML form data, multipart data, headers, query parameters, redirects, timeout, and selected network retries.
It is part of Playwright, but it does not need a visible browser page. That makes it suitable for API-only tests and fast setup around UI tests. A checkout test can create a cart through the API, open the browser at that cart, and verify only the workflow that matters. An API suite can exercise status codes, contracts, authorization, idempotency, and state transitions directly.
const response = await request.post('/api/projects', {
data: {
name: 'Payments regression',
owner: 'qa@example.com',
},
});
expect(response.status()).toBe(201);
expect(response.headers()['content-type']).toContain('application/json');
data accepts a serializable object and Playwright serializes it as JSON, adding an appropriate content type unless you override the header. A response is not automatically considered a test success just because the request completed. By default, a 400 or 500 still produces an APIResponse, so your test must assert status or use failOnStatusCode deliberately.
For a broader API quality strategy beyond the client mechanics, see the API testing interview and design guide.
2. Install and Configure a Runnable TypeScript Project
Initialize Playwright Test and choose TypeScript. The initializer installs the runner, browsers, configuration, and an example test.
npm init playwright@latest
npx playwright test
Configure a base URL and non-secret common headers in playwright.config.ts. Read credentials from environment variables rather than committing them.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: process.env.API_BASE_URL ?? 'http://127.0.0.1:3000',
extraHTTPHeaders: {
Accept: 'application/json',
},
},
});
The built-in request fixture respects relevant test options such as baseURL and extraHTTPHeaders. A relative URL such as /api/users is resolved against the configured base URL. Keep the leading slash policy consistent because standard URL resolution rules can produce surprising paths when the base contains a pathname.
Create an API smoke test:
import { test, expect } from '@playwright/test';
test('health endpoint is ready', async ({ request }) => {
const response = await request.get('/health');
expect(response.status()).toBe(200);
await expect(response).toBeOK();
const body = await response.json() as { status: string };
expect(body).toEqual({ status: 'ok' });
});
expect(response).toBeOK() checks that the status is in the successful 2xx range. Keep an exact status assertion when 200 versus 201 or 204 is part of the contract. A health endpoint that returns HTML from a reverse proxy should not pass merely because it is 200, so validate payload and content type too.
3. Choose Between the Request Fixture and newContext()
The fixture is the simplest option inside Playwright Test. The runner creates and cleans it for the test. It provides isolation and avoids custom lifecycle code.
test('lists active projects', async ({ request }) => {
const response = await request.get('/api/projects', {
params: { status: 'active', limit: 20 },
});
await expect(response).toBeOK();
});
Create a standalone context when a worker fixture, library, global setup, or specialized client needs different headers, credentials, proxy, storage state, or base URL.
import { request } from '@playwright/test';
const api = await request.newContext({
baseURL: 'http://127.0.0.1:3000',
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN ?? ''}`,
Accept: 'application/json',
},
});
try {
const response = await api.get('/api/me');
if (!response.ok()) {
throw new Error(`GET /api/me failed with ${response.status()}`);
}
} finally {
await api.dispose();
}
Responses are retained so their bodies can be read later. Disposing a context releases its resources, and methods called after disposal throw. Do not dispose the built-in fixture yourself or dispose browserContext.request independently from its owner.
Use one context per identity or isolation boundary rather than changing the Authorization header unpredictably on a shared helper. Explicit contexts make parallel tests safer and make request ownership visible during code review.
4. Send JSON, Query Parameters, Forms, and Files
Choose the request body option that matches the endpoint contract. data is commonly used for JSON. form sends application/x-www-form-urlencoded. multipart sends multipart form data and can include file-like values. params encodes query parameters separately from the path.
const created = await request.post('/api/users', {
data: { name: 'Asha Patel', role: 'tester' },
});
expect(created.status()).toBe(201);
const search = await request.get('/api/users', {
params: { role: 'tester', active: true, page: 1 },
});
await expect(search).toBeOK();
const token = await request.post('/oauth/token', {
form: {
grant_type: 'client_credentials',
client_id: process.env.CLIENT_ID ?? '',
client_secret: process.env.CLIENT_SECRET ?? '',
},
});
await expect(token).toBeOK();
For multipart upload, pass a file-like object with name, mimeType, and buffer. This is self-contained and does not depend on a file path.
const upload = await request.post('/api/documents', {
multipart: {
purpose: 'test-evidence',
document: {
name: 'evidence.txt',
mimeType: 'text/plain',
buffer: Buffer.from('verified by Playwright'),
},
},
});
expect(upload.status()).toBe(201);
Do not set Content-Type: multipart/form-data manually because the boundary must match the encoded body. Let Playwright generate it. Likewise, do not use JSON.stringify for a normal serializable object unless the endpoint specifically expects a raw string. Automatic serialization gives the correct content type and clearer intent.
5. Understand Authentication and Cookie Sharing
Authentication can use bearer headers, HTTP credentials, client certificates, or cookies, depending on the application. With a token API, set the Authorization header on a dedicated context or per request. Keep secrets in runtime configuration and redact them from attachments and logs.
import { test as base, request, type APIRequestContext } from '@playwright/test';
type ApiFixtures = { adminApi: APIRequestContext };
export const test = base.extend<ApiFixtures>({
adminApi: async ({}, use) => {
const adminApi = await request.newContext({
baseURL: process.env.API_BASE_URL,
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.ADMIN_API_TOKEN}`,
},
});
await use(adminApi);
await adminApi.dispose();
},
});
A request context associated with a BrowserContext shares its cookie storage. A Set-Cookie response received through browserContext.request updates browser cookies, and browser cookies are used by later API calls from that associated context. This enables fast session setup without copying cookie values manually.
test('logs in by API and opens the dashboard', async ({ browser }) => {
const context = await browser.newContext({
baseURL: process.env.APP_BASE_URL ?? 'http://127.0.0.1:3000',
});
try {
const login = await context.request.post('/api/login', {
data: { email: 'qa@example.com', password: process.env.E2E_PASSWORD },
});
await expect(login).toBeOK();
const page = await context.newPage();
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
} finally {
await context.close();
}
});
The try/finally ensures a failed assertion does not skip browser cleanup. For persistent reuse across tests, generate authenticated storageState in a setup project and keep account isolation clear.
6. Inspect APIResponse Without Hiding Failures
An APIResponse exposes status(), statusText(), ok(), url(), headers(), headersArray(), body(), text(), and json(). Parse the representation that matches the endpoint. A 204 response has no JSON body, so calling json() would be a test bug.
const response = await request.get('/api/orders/ORD-42');
expect(response.status()).toBe(200);
expect(response.headers()['content-type']).toContain('application/json');
const order = await response.json() as {
id: string;
status: 'pending' | 'paid' | 'cancelled';
total: number;
};
expect(order).toMatchObject({
id: 'ORD-42',
status: 'paid',
});
expect(order.total).toBeGreaterThan(0);
TypeScript assertions describe expected shape to the compiler but do not validate untrusted runtime data. For a strong contract suite, use explicit assertions or a schema validator your project already depends on. Validate required fields, types, invariants, and prohibited data rather than comparing enormous payloads blindly.
Failure messages should include safe diagnostic context. Status, method, path, correlation ID, and a redacted response excerpt are useful. Tokens, session cookies, passwords, and personal data are not. If response bodies are large, call apiResponse.dispose() after extracting what you need, or dispose the owning context at the proper scope.
By default, request methods return responses for error statuses. This behavior is excellent for negative testing because you can assert a structured 422 or 409 response. Set failOnStatusCode: true only when non-2xx and non-3xx should abort the operation rather than become an assertion subject.
7. Model Negative Tests and Error Contracts
Negative API tests should verify more than "not 200." Assert the exact status, stable error code, safe message, field-level details, and lack of an unintended state change. Send one meaningful invalid condition at a time so the failure has a clear cause.
test('rejects a duplicate email', async ({ request }) => {
const email = `duplicate-${Date.now()}@example.test`;
const first = await request.post('/api/users', {
data: { email, name: 'First user' },
});
expect(first.status()).toBe(201);
const duplicate = await request.post('/api/users', {
data: { email, name: 'Second user' },
});
expect(duplicate.status()).toBe(409);
const error = await duplicate.json() as {
code: string;
message: string;
};
expect(error.code).toBe('EMAIL_ALREADY_EXISTS');
expect(error.message).not.toContain('SQL');
});
Keep failOnStatusCode false for this test, which is the default. A thrown transport-style error would make it harder to assert the error body. For a deeper treatment of conflict semantics, use the HTTP 409 Conflict testing guide.
Authorization tests should cover missing identity, valid identity without permission, and valid identity with permission. Distinguish authentication from authorization and assert the application's chosen 401 and 403 contract. Confirm a rejected mutation did not alter the resource by reading it afterward through a permitted account.
Avoid using production-like personal data in error tests. Unique synthetic identifiers prevent parallel collisions, and deterministic cleanup keeps reruns safe.
8. Control Timeouts, Redirects, and Network Retries
An API request has a 30-second request timeout by default. Pass a local timeout for an endpoint with a documented service-level expectation, or configure the context default. Passing 0 disables the request timeout, which is rarely suitable for CI because a dead dependency can hold a worker indefinitely.
const response = await request.get('/api/reports/daily', {
timeout: 20_000,
maxRedirects: 0,
});
expect(response.status()).toBe(302);
Requests normally follow redirects, up to the supported default limit. maxRedirects: 0 is useful when the redirect itself is the contract under test. Inspect location and cookie headers rather than following to the final page.
maxRetries handles a narrow class of network errors. Current Playwright behavior retries ECONNRESET; it does not retry based on HTTP response status. A value of 2 does not make 503 responses automatically retry. Implement application-aware status retry only when the endpoint is safe to repeat, the policy is explicit, and the assertions can prove the attempt behavior.
const response = await request.get('/api/catalog', {
maxRetries: 2,
timeout: 10_000,
});
Never retry a non-idempotent payment or order creation blindly. Use idempotency keys and verify server semantics where a retry is required. The API idempotency testing guide covers duplicate delivery and safe retry cases.
Separate transport failures from HTTP failures in reports. A DNS error, TLS failure, connection reset, 503 response, and schema mismatch belong to different diagnostic categories even if all block the same test.
9. Combine API Setup with UI Verification Safely
API setup can make UI tests faster and more focused. Create only the prerequisite state through the API, then verify behavior through the browser. Cleanup through the API in a fixture so the test remains readable and repeatable.
import { test, expect } from '@playwright/test';
test('archives a project from its settings page', async ({ page, request }) => {
const create = await request.post('/api/projects', {
data: { name: `Archive me ${Date.now()}` },
});
expect(create.status()).toBe(201);
const project = await create.json() as { id: string; name: string };
try {
await page.goto(`/projects/${project.id}/settings`);
await page.getByRole('button', { name: 'Archive project' }).click();
await page.getByRole('dialog', { name: 'Archive project' })
.getByRole('button', { name: 'Confirm archive' })
.click();
await expect(page.getByRole('status')).toHaveText('Project archived');
} finally {
await request.delete(`/api/test-projects/${project.id}`);
}
});
The cleanup endpoint is intentionally test-specific in this example. In real systems, use authorized cleanup that cannot affect resources outside the test namespace. Make cleanup tolerate an already deleted or partially created resource.
Do not create state through an internal implementation detail that bypasses the rules you are trying to verify. If the scenario tests public API creation, use that API. If it tests UI creation, do not replace the behavior under test with API setup. The boundary should make the test narrower without removing its purpose.
API and UI identities must also align. An isolated request fixture does not automatically share cookies with the page. Use token-based endpoints, storage state, or browserContext.request deliberately.
10. Production Checklist for playwright APIRequestContext
Organize API clients around business resources, not one giant wrapper. A ProjectsApi can accept an APIRequestContext, implement request construction, and return the raw APIResponse or a deliberately validated domain result. Tests should still make the contract assertions visible.
Keep contexts isolated by actor, tenant, and parallel worker. Use synthetic unique data, explicit cleanup, and idempotent teardown. Never share a mutable account when tests change its permissions or preferences concurrently.
Apply this review checklist:
- Base URL and common headers are configured once at an understandable scope.
- Secrets come from the environment and never enter source, traces, or failure excerpts.
- Exact status and meaningful payload invariants are asserted.
- Negative responses remain inspectable instead of being converted into opaque exceptions.
- Standalone contexts are disposed in teardown or
finally. - Browser-shared contexts are used only when cookie sharing is intentional.
- Request timeouts and retries reflect endpoint semantics.
- Test data is unique, minimal, and cleaned safely.
- API setup does not bypass the behavior the test claims to cover.
Treat client helpers as test infrastructure with code review, types, and focused tests. A wrapper that silently retries, ignores status, or catches every error can make failures harder to diagnose than direct calls. Favor small abstractions that remove repeated mechanics while preserving HTTP evidence.
Interview Questions and Answers
Q: What is APIRequestContext in Playwright?
It is an HTTP client context that sends requests and maintains its own configuration and cookie state. It supports API-only tests, data setup, authentication, and backend verification around browser tests. It returns APIResponse objects that expose status, headers, and body methods.
Q: What is the difference between the request fixture and request.newContext()?
The Playwright Test fixture is runner-managed and convenient for test-scoped isolated requests. A context created with newContext() is explicitly configured and owned by your code, so you must dispose it. I use the latter for worker fixtures, utilities, or separate identities.
Q: Does APIRequestContext throw for a 404 or 500 response?
Not by default. It returns an APIResponse, which lets negative tests assert status and error payload. failOnStatusCode: true changes that behavior for responses outside 2xx and 3xx.
Q: How do API calls share authentication with a browser?
browserContext.request shares cookie storage with its owning browser context. Cookies set by an API response can authenticate later page navigation, and browser cookies are available to associated API calls. A separately created request context or the isolated request fixture does not automatically share those cookies.
Q: How do you validate a JSON response safely?
I first assert status and content type, then parse JSON and validate required fields, types, and business invariants. A TypeScript type assertion helps editor and compiler use but does not validate runtime data. Contract-critical suites should use explicit assertions or an approved runtime schema library.
Q: What does maxRetries retry?
It retries selected network errors, currently ECONNRESET, up to the configured maximum. It does not retry 429, 500, or 503 responses. Status-based retry needs an explicit, idempotency-aware policy.
Q: Why must a standalone APIRequestContext be disposed?
The context retains response resources so response bodies remain available. dispose() releases those resources and ends the context lifecycle. I place it in fixture teardown or a finally block so failures do not leak resources.
Q: When should API setup be used in a UI test?
Use it for prerequisites outside the UI behavior being tested, such as creating a project before testing archive controls. It reduces duration and failure surface. Do not use it to bypass the exact creation or validation path that the scenario claims to verify.
Common Mistakes
- Assuming every non-2xx response throws automatically.
- Calling
json()on a 204 response or a non-JSON error page. - Treating a TypeScript cast as runtime schema validation.
- Creating standalone request contexts without disposing them.
- Manually setting a multipart content type and breaking its boundary.
- Logging bearer tokens, cookies, credentials, or sensitive response data.
- Sharing one mutable account across parallel tests.
- Expecting an isolated request fixture to share page cookies.
- Enabling retries for unsafe mutations without idempotency protection.
- Using API setup to bypass the behavior the test is supposed to exercise.
Conclusion
playwright APIRequestContext gives QA engineers a capable HTTP client inside the same runner used for browser automation. Start with the request fixture, configure base URL and headers centrally, and create custom contexts only when identity, storage, or lifecycle requires them.
Build one focused API test that asserts status, headers, and payload, then use the same client for narrowly scoped test setup. Keep cookie ownership, cleanup, negative responses, timeouts, and retry safety explicit. Those practices produce fast API coverage without sacrificing trustworthy diagnostics.
Interview Questions and Answers
What problems does APIRequestContext solve?
It provides an HTTP client within the Playwright ecosystem for API testing, setup, cleanup, authentication, and postcondition checks. It avoids browser overhead when no UI behavior is needed. It also supports cookie sharing when obtained from a browser context.
How does the request fixture differ from browserContext.request?
The request fixture is isolated and runner-managed for the test. `browserContext.request` shares the browser context cookie jar, so API and page sessions influence each other. I choose based on whether cookie sharing is part of the scenario.
How do you test a negative API response?
I keep `failOnStatusCode` false, assert the exact status, and validate the stable error contract. I also verify that a rejected mutation did not change server state. This distinguishes a correct rejection from a misleading error response.
How do you validate JSON beyond TypeScript types?
A TypeScript cast does not validate runtime input. I assert required fields and invariants directly or use an approved runtime schema validator. I also check status and content type before parsing.
What does APIRequestContext maxRetries do?
It retries a narrow network error category, currently including `ECONNRESET`. It does not retry HTTP 429, 500, or 503 responses. Status-based retry needs explicit policy and idempotency analysis.
How do you prevent API test data collisions?
I generate unique data per test or worker and keep resources inside a test namespace. Cleanup is idempotent and runs in `finally` or fixture teardown. I avoid shared mutable accounts and fixed resource names during parallel execution.
When should API setup be used in a browser test?
I use it for prerequisites outside the UI behavior being tested. It makes the browser flow narrower and faster while preserving the user action under test. I do not bypass the exact path the scenario claims to verify.
Why is disposal important for standalone contexts?
The request context retains response resources so bodies remain readable. Disposing it releases those resources and closes the lifecycle. I guarantee disposal with fixture teardown or `finally` even when assertions fail.
Frequently Asked Questions
What is Playwright APIRequestContext?
It is Playwright's HTTP client context for sending API requests and managing request configuration and cookies. It supports API-only tests, test-data setup, authentication, and backend checks around browser tests.
Should I use the request fixture or request.newContext?
Use the built-in fixture for normal test-scoped isolated calls because the runner manages it. Use `newContext()` for custom identity, storage, or lifecycle, and dispose that context yourself.
Does APIRequestContext share cookies with the browser?
`browserContext.request` shares cookie storage with its browser context. A separately created API request context and the standard isolated request fixture do not automatically share page cookies.
Does Playwright throw on API status 400 or 500?
Not by default. It returns an `APIResponse`, which allows negative tests to assert the status and error body. Set `failOnStatusCode: true` only when those statuses should abort the operation.
How do I send JSON with APIRequestContext?
Pass a serializable object through the `data` option of `post`, `put`, `patch`, or `fetch`. Playwright serializes it as JSON and sets the content type unless you explicitly override it.
Do I need to dispose APIRequestContext?
Dispose contexts that your code creates with `request.newContext()`. The Playwright Test request fixture and contexts owned by a browser lifecycle should be cleaned up by their respective owners.
Can APIRequestContext upload a file?
Yes. Use the `multipart` option with a stream or a file-like object containing `name`, `mimeType`, and `buffer`. Let Playwright generate the multipart content type and boundary.