Playwright is best known for browser automation, but it also has API testing built in. With APIRequestContext, you can write GET, POST, PUT, and DELETE tests without installing anything extra. This article covers how to get started with API testing using nothing but Playwright’s own built-in feature โ from basic requests to combining it with your UI tests.
It’s easy to assume API testing always means reaching for a separate library. If you’re already working in a pytest-playwright setup, you can start writing API tests without adding requests at all.
๐ Who This Article Is For
- Anyone comfortable with Playwright browser automation who hasn’t written an API test yet
- Anyone curious about the difference between Python’s requests library and Playwright’s API testing
- Anyone who wants to combine UI tests and API tests in the same test code
- Anyone who wants to start simple, with basic GET and POST requests
โ What You’ll Learn
- How to write basic requests using Playwright’s APIRequestContext
- How to verify response status, headers, and JSON body
- The practical benefits of combining UI tests and API tests
๐ค About the Author
Written by Yoshi, a QA and test automation engineer with over 15 years of hands-on experience implementing both UI and API tests with Playwright, and designing test suites that combine the two.
๐ Key Takeaways
- With a pytest-playwright setup, Playwright’s
APIRequestContextlets you write GET, POST, PUT, and DELETE tests without addingrequests - Sharing cookies and session state with the browser is a strength Playwright’s API testing has that the requests library doesn’t
- A test like “log in via UI, then verify via API” can happen entirely within a single shared session
Working on a Playwright-based UI test project, you’ll sometimes run into situations like these:
- You need to set up a large volume of test data via API as a precondition for UI tests
- You want to verify the result of a UI action precisely, by checking the API response
- You’d rather not add a separate library (like requests) purely for API testing
Playwright’s APIRequestContext feature lets you handle all of this with the same tool, inside the same test code.
Where This Article Fits
This is an introductory article for anyone writing their first Playwright API test, focused on the basics of making GET, POST, and similar requests. More advanced, production-oriented topics โ authentication flows, token retrieval, security verification โ are covered in a separate, dedicated article.
requests vs. Playwright’s APIRequestContext
The requests library is the standard choice for API testing in Python, but there are good reasons to reach for Playwright’s own API testing feature instead when you’re already working in a Playwright-based project.
| Consideration | requests | Playwright APIRequestContext |
|---|---|---|
| Extra installation | Required (pip install requests) | Not required (built into pytest-playwright’s APIRequestContext) |
| Ease of writing simple API tests | โ (simple, well-documented) | โ |
| Sharing browser cookies/session | โ (you’d have to pass it through yourself) | โ
(shareable via context.request) |
| Integration with UI tests | Tends to become a separate tool with separate management | โ (lives inside the same test file) |
requests‘s simplicity is a genuine advantage. But if you want to combine API tests with UI tests, or reuse a browser’s logged-in state to call an API, Playwright’s APIRequestContext is the better fit.โ A Basic GET Request
Here’s the basic form, using the playwright fixture that pytest-playwright provides, to send an independent API request.
from playwright.sync_api import Playwright
def test_get_user(playwright: Playwright) -> None:
# Create a dedicated API request context with a base_url
request_context = playwright.request.new_context(
base_url="https://jsonplaceholder.typicode.com"
)
response = request_context.get("/users/1")
assert response.status == 200
assert response.ok # True for any status in the 200โ299 range
body = response.json()
assert body["id"] == 1
# Explicitly dispose of the context once you're done with it
request_context.dispose()request_context.dispose() leaves the request context’s resources hanging around. Creating and disposing of it through a fixture, rather than managing it manually every time, helps prevent forgetting this step.Reusing it via a fixture
# conftest.py
import pytest
from playwright.sync_api import Playwright, APIRequestContext
@pytest.fixture
def api_context(playwright: Playwright):
context = playwright.request.new_context(
base_url="https://jsonplaceholder.typicode.com"
)
yield context
context.dispose() # Automatically disposed after the test finishes# test_users.py
def test_get_user(api_context):
response = api_context.get("/users/1")
assert response.status == 200โก POST, PUT, and DELETE Requests
Other HTTP methods can be called just as intuitively from the same APIRequestContext.
def test_create_user(api_context):
# POST: create a new resource
response = api_context.post("/users", data={
"name": "Taro Yamada",
"email": "taro@example.com",
})
assert response.status == 201
created = response.json()
assert created["name"] == "Taro Yamada"
def test_update_user(api_context):
# PUT: full update
response = api_context.put("/users/1", data={
"name": "Updated Name",
"email": "updated@example.com",
})
assert response.status == 200
def test_delete_user(api_context):
# DELETE: remove a resource
response = api_context.delete("/users/1")
assert response.status == 200data= automatically gets serialized to JSON by Playwright and sent as the request body. Not having to manually set the Content-Type header is a nice bit of convenience compared to the requests library. If you need to send an application/x-www-form-urlencoded (form-encoded) request instead, use form= in place of data= โ pick whichever fits your use case.โข Verifying the Response
Here’s a summary of the main properties and methods you can pull off a response object.
| Property / Method | What it gives you |
|---|---|
response.status | The HTTP status code (as an integer) |
response.ok | A boolean for whether the status is in the 200s |
response.headers | The response headers, as a dictionary |
response.json() | The response body, parsed as JSON |
response.text() | The response body as a plain string |
def test_response_details(api_context):
response = api_context.get("/users/1")
# Status code
assert response.status == 200
# Partial-match header check (an exact match tends to fail once a charset is added)
content_type = response.headers.get("content-type", "")
assert "application/json" in content_type
# Confirm required fields are present
body = response.json()
for field in ["id", "name", "email"]:
assert field in body, f"'{field}' is missing from the response"โฃ Combining UI and API Tests
The biggest strength of Playwright’s API testing is that the browser context and the API request context can share cookies and session state. You get this by using context.request โ the request property that belongs to your browser context โ instead of playwright.request.
| Browser context Logs in via page.goto() | cookies shared | context.request Calls the API with the same cookies |
playwright.request.new_context() creates a context that’s independent of the browser context, so cookies are not shared (compare it with the code below).def test_ui_login_then_verify_via_api(page, context):
# โ Log in via the UI
page.goto("/login")
page.get_by_label("Email").fill("test@example.com")
page.get_by_label("Password").fill("Password123")
page.get_by_role("button", name="Log in").click()
page.wait_for_url("**/dashboard")
# โก Use the same session (cookies) to check the profile via the API
# context.request shares cookies with the browser
response = context.request.get("/api/profile")
assert response.status == 200
profile = response.json()
assert profile["email"] == "test@example.com"Common Mistakes Seen in Practice
โ Confusing playwright.request with context.request
playwright.request.new_context() creates a fresh, independent context, while context.request shares cookies with your browser context. Mixing these up based on your actual goal (an isolated API test vs. one tied to your UI session) leads to a session state you didn’t expect, and can cost real time in investigation.
โก Forgetting to call request_context.dispose()
When you’re managing a context created with playwright.request.new_context() manually, forgetting dispose() leaves its resources lingering. Designing it around a fixture’s yield pattern, so disposal happens automatically, is the safer approach.
โข Using an exact-match header check that breaks unexpectedly
Checking with response.headers["content-type"] == "application/json" fails in practice whenever a charset gets appended, like application/json; charset=utf-8. A partial match using in is the safer check.
โฃ Sticking with Playwright’s APIRequestContext even when it’s purely API testing
If you don’t need any UI integration and you’re writing a large, purely API-focused test suite, forcing yourself to stick with Playwright often means more code than the simpler requests library would need. Choosing the right tool for the job matters here too.
๐ Related Articles
FAQ
Q. Should I use requests or Playwright’s API testing?
If you’re writing a large, systematic suite of purely API-focused tests, the simplicity of requests is a real advantage. If you want to write API tests alongside UI tests in the same project, or reuse a browser’s login session, Playwright’s APIRequestContext is the better fit. Choose based on your project’s primary goal.
Q. What’s the difference between playwright.request and context.request?
playwright.request.new_context() creates a brand-new, fully independent API request context. context.request, on the other hand, lets you send API requests while sharing the cookies and session state already held by an existing browser context. Use the latter when you want your UI and API activity to work together.
Q. How should I validate a response’s schema?
The basic approach is checking each field’s type with isinstance() against the dictionary you get from response.json(). For stricter schema validation, you might consider pairing it with a dedicated library like jsonschema.
Q. Do I have to set base_url when creating a new_context?
No, base_url isn’t required. If you skip it, you’ll need to specify the full URL on every request. If you’re sending multiple requests to the same API, setting base_url up front keeps your code noticeably cleaner.
Q. Can base_url be managed through an environment variable?
Yes. Instead of hardcoding a string like base_url="https://api.example.com", reading it from an environment variable โ e.g. os.environ["API_BASE_URL"] โ lets you switch between staging and production without touching your test code. Managing this through a .env file with python-dotenv is also a common pattern.
Q. What if I need to test an API that requires authentication?
Token retrieval, attaching authentication headers, and other authentication-flow topics are outside the scope of this article. Our related article on automating authentication-flow API tests with Playwright and Python covers this in depth โ check that out for the details.
๐ Summary
- With a pytest-playwright setup, Playwright’s APIRequestContext lets you write GET, POST, PUT, and DELETE tests without adding
requests - context.request lets you call an API while sharing the browser’s logged-in session
- In practice: reach for requests when it’s purely API testing, and Playwright when you need UI integration
- More advanced topics like authentication flows are covered in a related article
If you’re already using Playwright for UI tests, you don’t need to learn a new tool just to test APIs. Start with a simple GET request and try weaving it into your existing test code.
