How To Automate API using Playwright

A
Artem Bondar
8 min read

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:

  • 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.

demo.spec.ts
123456789101112131415161718
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 data property and should be a valid JSON object

  • API request headers are passed under headers property, 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:

demo.spec.ts
1234567
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.

demo.spec.ts
123456789101112131415161718
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:

demo.spec.ts
12345678
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:

apiTest.spec.ts
1234567891011121314151617181920212223242526272829303132333435
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:

fluentDesign.spec.ts
12345678910111213141516171819202122232425
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.

Artem Bondar

About the Author

Hey, this is Artem - test engineer, educator, and the person behind this academy.

I like test automation because it drastically reduces the workload of manual testing. Also, it's a lot of fun when you build a system that autonomously does your job.

Since 2020, I have been teaching how to use the best frameworks on the market, their best practices, and how to approach test automation professionally. I enjoy helping QAs around the world elevate their careers to the next level.

If you want to get in touch, follow me on X, LinkedIn, and YouTube. Feel free to reach out if you have any questions.