Playwright is the No. 1 framework for UI test Automation. But it also has API testing capabilities! Using a single framework, you can test both the UI and the API for your web application.
In this article, I’ll demonstrate how you can do it. You can also write the code along the way and see Playwright in action.
Ready?
Setting up the Development Environment
Before we dive into the code, let’s set up the environment. Make sure Node.js is installed on your machine. Then, you can create a new project and install Playwright with TypeScript.
If Node.js sounds unfamiliar, I would recommend watching the Introduction module or the Playwright UI Testing Mastery program; both are free. You just need to create a new account. In this section, I show how to configure the development environment and install Playwright.
If you feel confident and just need quick guidance, here is what you need to do:
Navigate to https://nodejs.org/ and install the current LTS version of Node.js
Create a new folder on your computer for the project
Open this folder in VS Code
Open a new terminal and run the command npm init playwright@latest
Follow the default suggestions in the terminal and just hit “Enter” for everything until installation is complete.
That's it!
You are ready to write API tests with Playwright.
Request Fixture
Playwright has a concept of “fixtures”. Fixture - is the method or function that can be executed before the test and after the test (in simple words). You can create fixtures by yourself.
There are also 4 pre-defined Playwright fixtures:
Page
Request
Context
Browser
We are interested in only two of those:
Page - is a fixture for UI Automation. This fixture creates a new instance of the web browser where the test is executed
Request - is a fixture for API automation. This fixture provides methods for interacting with APIs.
Yes, that simple :)
So, to write API tests in Playwright, you need to use a Request fixture. When you use it, Playwright does not launch the browser to make API requests.
You can use both fixtures in a single test, automating web browser operations and making API requests when needed.
"Request" is a fixture for API automation. This fixture does not launch the browser. It only makes API calls.
CRUD operations in Playwright
CRUD is an acronym for Create, Read, Update, and Delete. Each of those operations can be done using API request methods:
POST: Create a record on the API resource
GET: Read the record from the API resource
PUT: Update the existing record on the API resource
DELETE: Self-explanatory, this method deletes the record from the API resource
Now let's review those operations one by one in Playwright.
POST request
Here is an example of a POST request in Playwright to create a new Article in the Conduit application. To interact with API, we use a separate API URL for this test application.
Keep in mind
The application URL and the API URL are often different. You can't open the API URL in the web browser, nor can you can't send an API request to the application URL.
test('Create Aricle', async ({ request }) => { const newArticleResponse = await request.post('https://conduit-api.bondaracademy.com/api/articles/', { data: { "article": { "title": "Article Title", "description": "Test description", "body": "Test body", "tagList": [] } }, headers: { Authorization: 'Bearer <token_value>' } }) const newArticleResponseJSON = await newArticleResponse.json() expect(newArticleResponse.status()).toEqual(201) expect(newArticleResponseJSON.article.title).toEqual('Article Title')})
Notice that we use a request fixture, passed as an argument to the test. This fixture gives us access to API request methods
We call post() method to make a POST request. We pass 3 arguments: API URL, Request object, and Headers.
The API URL is passed as a string
Request object is passed under
dataproperty and should be a valid JSON object
API request headers are passed under
headersproperty, and also should be a JSON object
When the API request completes, the value must be assigned to a constant or va. iable. Then, to extract the JSON response object from the response, need to call a json() line 15.
Caution
It's important to use the await method before calling Playwright commands. Otherwise, the code will not work properly.
After the response object is received, you can perform a validation using build in expect() method to validate JSON properties of the object or validate the status code.
GET request
The simplest API request. Just read the information from API resource. Here is the code example that you can execute on your computer:
test('Get Test Tags', async ({ request }) => { const tagsResponse = await request.get('https://conduit-api.bondaracademy.com/api/tags') const tagsResponseJSON = await tagsResponse.json() expect(tagsResponse.status()).toEqual(200) expect(tagsResponseJSON.tags[0]).toEqual('Test')});
In this test, we request the list of available tags for our test application. When it should be a secure API request, you can also pass the headers object with the Authorization token, as shown in the example above for the POST request.
PUT request
A PUT request updates an existing record on an API resource. For example, the code below updates the article title created in the first example via a POST request.
test('Update Aricle', async ({ request }) => { const updateArticleResponse = await request.put(`https://conduit-api.bondaracademy.com/api/articles/Article-Title-123`, { data: { "article": { "title": "NEW Article Title", "description": "Test description", "body": "Test body", "tagList": [] } }, headers: { Authorization: 'Bearer <token_value>' } }) const updateArticleResponseJSON = await updateArticleResponse.json() expect(updateArticleResponse.status()).toEqual(200) expect(updateArticleResponseJSON.article.title).toEqual('NEW Article Title')})
To update the article, we append the Article ID to the request URL. This way, we tell our API which article we want to update. Then, for this request, we use the put() method and provide a new request object with the updated article title.
DELETE request
The DELETE request method deletes the resource on the API. Very straightforward. Just provide the resource ID to delete and make the request. Here is the example:
test('Delete Aricle', async ({ request }) => { const deleteArticleResponse = await request.delete(`https://conduit-api.bondaracademy.com/api/articles/Article-Title-123`, { headers: { Authorization: authToken } }) expect(deleteArticleResponse.status()).toEqual(204)})
Best Practice
It's considered best practice to perform a GET request after the DELETE request to properly validate the deletion of the resource
API Testing Framework
Playwright was not optimized for API testing. The API's capabilities primarily complement functional UI automation.
Using APIs, you can better control test data during test runs. For example, faster creation of the preconditions for UI test execution. Or deleting test data that is no longer needed.
If you would like to use Playwright for API testing, you definitely can, but you should build a custom framework around it. With this approach, it's a true all-in-one solution for UI and API testing. 100%!
While experimenting with Playwright and learning from popular API testing frameworks like REST-Assured and Karate, I came up with a very convenient idea: how to organize a test framework using the Fluent Interface Design pattern.
Here is an example of the code, written in pure Playwright scripting to create and delete an article:
test('Create and Delete Aricle', async ({ request }) => { const newArticleResponse = await request.post('/articles', { data: { "article": { "title": "Test TWO TEST", "description": "Test description", "body": "Test body", "tagList": [] } }, headers: { Authorization: authToken } }) const newArticleResponseJSON = await newArticleResponse.json() expect(newArticleResponse.status()).toEqual(201) expect(newArticleResponseJSON.article.title).toEqual('Test TWO TEST') const slugId = newArticleResponseJSON.article.slug const articlesResponse = await request.get('/articles?limit=10&offset=0', { headers: { Authorization: authToken } }) const articlesResponseJSON = await articlesResponse.json() expect(articlesResponse.status()).toEqual(200) expect(articlesResponseJSON.articles[0].title).toEqual('Test TWO TEST') const deleteArticleResponse = await request.delete(`/articles/${slugId}`, { headers: { Authorization: authToken } }) expect(deleteArticleResponse.status()).toEqual(204)})
And this is the same test, but written using Fluent Interface Design:
test('Create and Delete Article', async ({ api }) => { const createArticleResponse = await api .path('/articles') .body({ "article": { "title": "Test TWO TEST", "description": "Test description", "body": "Test body", "tagList": [] } }) .postRequest(201) await expect(createArticleResponse).shouldMatchSchema('articles', 'POST_articles') expect(createArticleResponse.article.title).shouldEqual('Test TWO TEST') const slugId = createArticleResponse.article.slug const articlesResponse = await api .path('/articles') .params({ limit: 10, offset: 0 }) .getRequest(200) expect(articlesResponse.articles[0].title).shouldEqual('Test TWO TEST') await api .path(`/articles/${slugId}`) .deleteRequest(204) const articlesResponseTwo = await api .path('/articles') .params({ limit: 10, offset: 0 }) .getRequest(200) expect(articlesResponseTwo.articles[0].title).not.shouldEqual('Test TWO TEST')})
Did you notice how compact and easier to read it is?
The test framework provides a dedicated method for performing specific operations on data values, which you can chain together using "dot notation.":
path() - To provide a path of the API resource that should be called
params() - To provide URL query parameters for the request URL
body() - To provide a JSON request object for PUT or POST request
getRequest() - To make a GET request and automatically validate the status code
postRequest() - To make a POST request and automatically validate the status code
putRequest() - To make a PUT request and automatically validate the status code
deleteRequest() - To make a DELETE request and automatically validate the status code
This convenient separation of responsibilities makes scripting easier, with fewer lines of code to write, and easier maintenance.
Learn More
If you want to learn more about how to build a robust and scalable API testing framework using Playwright, please check the Playwright API Testing Matery program.
This course covers everything you need to know about API automation. It starts from scratch, covering API testing fundamentals, and gradually progresses to advanced techniques in framework setup: Fluent Interface Design, Schema Validation, Logging and Reporting, and much more.
Frequently Asked Questions
Can Playwright be used for UI and API testing at once
Yes. Playwright has different fixtures for UI and API automation. So you can combine UI and API testing as part of the same framework
Do I need to run a browser for API testing in Playwright
No, Playwright uses a separate "request" fixture for API calls, which does not run the browser. This makes test execution fast.
Can I run API tests in parallel?
Absolutely! It's free because Playwright is a fully open-source framework. The number of parallel threads is limited only by your CPU and your API's performance.
Which tests are usually more reliable, UI or API tests?
API tests are more reliable because they don't depend on the browser's user interface. It's just a data flow.

