The Playwright framework is known for its UI testing features, but it also has robust tools for testing API capabilities. Playwright API testing tools allow you to build faster and more reliable end-to-end tests. Because you can combine both the UI and API tests into one test suite, your testing efforts become more efficient.
In this tutorial, you will learn to set up the Playwright APIRequestContext, authentication persistence and how to integrate CircleCI to ensure that your tests can run in a CI/CD platform.
Prerequisites
Before you begin, you will need:
- Node.js (version 16 or later) installed on your machine.
- CircleCI account: You can sign up for a free CircleCI account if you don’t have one.
- GitHub account: Your project will need to be on GitHub to connect it to CircleCI.
- Git installed in your machine.
- Basic knowledge of Playwright: Familiarity with writing basic Playwright tests will be helpful.
- Familiarity with CI/CD: A basic understanding of continuous integration and delivery concepts will be beneficial.
Start by cloning the Github repository from here.
Playwright’s APIRequestContext
End to End (E2E) testing does not tell the whole story of testing or what needs to be done. In many applications, user flows are easily tested efficiently at the API level. A good example is when you need to create authentication for every test within the login and signup module. Instead of manually filling in the details in both the signup and login forms, you can directly call the signup API, create the user, and use the newly created user to log into the system. This approach is not only faster but also more rigid and less prone to flakiness.
Playwright’s APIRequestContext plays a role to bridge the API requirements and needs of users of Playwright. It is a built-in module in the Playwright test framework that allows you to send HTTP(S) requests to a server. It works as a a lightweight version of axios or fetch but with a few extras. That it is built into Playwright means there is always consistency between the UI and the API in the tests being executed. This diagram shows the role of the APIRequestContext in the context of API calls in Playwright.

APIRequestContext also shares the same context with the browser. This makes it possible to share the storage sessions from the UI tests as well as the cookies from the browser with the API tests. The shared context makes it extremely easy to manage states and authentication in tests.
You can make an API call to log into a test and then open a browser page that is already logged in using the same session. With APIRequestContext, it is also possible to use auto-retries and Playwright helpers. It extends the same functionality that is used in Playwright.
Not only can you create API robust tests but you can extend the usage of your tests. You can efficient test suites by using APIs to set up the state of your tests and using the browser automation to test the UI bits of your application.
Now that you know about APIRequestContext, you can set it up in Playwright.
Setting up Playwright’s APIRequestContext
Setup of the APIRequestContext starts in the playwright.config.ts file by defining a baseURL to be used by all the API requests. This ensures that all endpoints can be called without declaring a baseURL in individual tests. You can override this variable in individual tests if you need it to run with a different baseURL.
The next code snippet shows a configuration of the playwright.config.ts for a simple application with a set of endpoints under https://api-testing-with-playwright-b1gd.vercel.app/:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// All API requests will be prefixed with this baseURL
baseURL: 'https://api-testing-with-playwright-b1gd.vercel.app',
// If you want to run tests against local server you can use
// baseURL:localhost:3000
// or any other port where your application is running
extraHTTPHeaders: {
// We can add headers that are common for all requests.
// For example, an authorization token.
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Content-Type': 'application/json',
},
},
});
Note: You can find this snippet in the root of the playwright-tests directory of the cloned repository.
Here’s a breakdown of this configuration:
baseURLspecifies the base URL for all API requests made with request. If you make a request to/usersendpoint, Playwright will callhttps://api.yourapp.com/api/users. It will add thebaseURLas a prefix to the endpoint that has been provided in the test.extraHTTPHeadersallows you to define headers that will be sent with every single API request. This is a great place for static tokens orcontent-typedeclarations. The{process.env.API_TOKENdoes not need to be supplied in this case. It is just a placeholder for using Bearer tokens in your authentication.
Sending API requests
Once you have setup the APIRequestContext, you can now start making API requests directly in your test files. Playwright provides a request fixture that you can use to send different types of requests such as GET, POST, PUT, DELETE, and other HTTP requests.
Here’s a snippet of a test file that uses your request fixture to fetch a list of users and create a new one. This snippet can be found in the playwright-tests/tests directory.
// tests/api.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Users API', () => {
// Test to get all users
test('should be able to get a list of users', async ({ request }) => {
const response = await request.get('/users');
// Assert that the request was successful
expect(response.ok()).toBeTruthy();
// Assert that the response body is an array
const responseBody = await response.json();
expect(Array.isArray(responseBody)).toBeTruthy();
});
// Test to create a new user
test('should be able to create a new user', async ({ request }) => {
const newUser = {
name: 'John Doe',
email: `john.doe.${Date.now()}@example.com`,
};
const response = await request.post('/users', {
data: newUser,
});
// Assert that the request was successful (e.g., 201 Created)
expect(response.status()).toBe(201);
expect(response.ok()).toBeTruthy();
// Assert that the response contains the created user's data
const responseBody = await response.json();
expect(responseBody.name).toBe(newUser.name);
expect(responseBody.email).toBe(newUser.email);
expect(responseBody.id).toBeDefined();
});
});
In the example above, you can see how intuitive it is to work with the request fixture in your tests. You have access to the request.get() and request.post() functions to make your API calls. You can then use expect assertions on the response object to verify the status code (response.ok(), response.status()) and the response body (response.json()).
This is a very powerful way to test your API’s core functionality in isolation. Testing your APIs this way ensures that your backend behaves as expected before you even touch the UI that executes on the browser tests.
Persisting Authenticated State
Now that you know that you can combine both API and UI tests in playwright by using a shared state, it might not be obvious but you can also have a persisted authenticated state in Playwright. In this section, you shall dive into how you can be able to achieve this in Playwright. In simple terms this would mean a login with your API endpoints and then using your UI tests using the authenticated user via API. To achieve this these are the steps you would take in your tests
- Make a POST request to your login endpoint.
- The server responds with an authentication token, usually in a
cookieor in theresponsebody. - Save this authentication state (cookies, local storage) to a file using
request.storageState(). - Configure your browser tests to use this saved authentication state.
Using the cloned repository, go to the playwright-tests/global.setup.ts file to review the authentication process.
// global.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = "./userAuth.json";
setup('authenticate', async ({ request }) => {
// Send a request to log in the user.
await request.post('/api/login', {
data: {
username: 'testUsername',
password: 'testP@ssword',
},
});
// Save the storage state to the userAuth.json.
await request.storageState({ path: authFile });
});
This step will run before all tests and after the login is successful, it will store the authentication token in the userAuth.json file, you can then access the token in your subsequent tests without the need of having to login every single time you want to run the tests. After you have logged in successfully, Playwright needs to know how to use this setup file and that you can configure in the playwright-tests/playwright.config.ts, these changes can be viewed in the cloned repository as shown below.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
//... other configs
// Use a global setup file for authentication
globalSetup: require.resolve('./global.setup.ts'),
use: {
//... other use options
// Use the saved authentication state for all tests
storageState: 'playwright/auth/userAuth.json',
},
});
Now, when all UI tests are executed, Playwright will first load all the cookies into the local storage from the userAuth.json file and use that to execute your tests.
Tip: It is important to know the length of the cookie session configuration, otherwise when a timeout occurs, Playwright needs to know when to re-authenticate the user and verify that the cookie remains active during the running of the tests, if this is not present, your tests may fail due to failed authentication due to an expired cookie
The snippet below is a test that benefits from the configuration above
// playwright-tests/tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test('user can access their dashboard', async ({ page }) => {
// The user is already logged in thanks to global setup! :)
await page.goto('/dashboard');
// Assert that the user's name is visible on the page
await expect(page.locator('h1')).toContainText('Welcome, Test User!');
});
Using the state persisitence approach not only makes your tests faster but also decouples your UI tests from the login flow, making them more focused and less brittle. Now that you have learned how to persist authenticated state, you can start running your tests in the CI/CD using CircleCI.
Note: If you have already cloned the repository, this step is optional. If you add CircleCI into another project, you can use the next configuration snippet.
Create a .circleci/config.yml file in the root directory of your project. This file will define the steps to check out your code, install dependencies, and run your Playwright tests. Here is a sample config.yml file designed for running Playwright tests:
version: 2.1
orbs:
circleci-cli: circleci/circleci-cli@0.1.9
jobs:
build:
docker:
- image: mcr.microsoft.com/playwright:v1.52.0-jammy
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: cd playwright-tests && npm install
- run: cd playwright-tests && npx playwright install
- run: cd playwright-tests && npm run test
- store_artifacts:
path: ./playwright-report
destination: playwright-report-first
Here’s a breakdown of this configuration file:
version: 2.1specifies the CircleCI version.orbsare reusable packages of CircleCI configuration. The circleci/node orb simplifies your Node.js setup.jobsdefine the tasks to be executed. You have one job namedtest.stepsare the individual commands to be run.checkoutchecks out your source code from the repository.node/install-packagesis a command from the node orb that installs your project’s dependencies (npm install).npx playwright install --with-depsis a crucial step that downloads the browser binaries that Playwright needs to run tests.--with-depsensures that all necessary OS-level dependencies are installed.npm run testexecutes your Playwright tests.store_artifactssaves the Playwright HTML report, which you can view in the on the Artifacts tab of your CircleCI job.
Using this configuration, every time you push a change to your GitHub repository, CircleCI triggers a build, installs everything needed, and runs your full suite of UI and API tests.
Now that you have a usable CircleCI configuration, you can add your project to CircleCI.
Setting up a project on CircleCI
If you cloned the sample project repository, your project is already integrated with CircleCI. If you started creating this project from scratch instead, you’ll need to initialize a GitHub repository in your project by running:
git init
Create a .gitignore file in the root directory. Inside the file, add node_modules to prevent npm-generated modules from being added to your remote repository. Add a commit and then push your project to GitHub.
Log into CircleCI and go to Projects. All the repositories associated with your GitHub username or your organization are listed; for this project, use api-testing-with-playwright.

Click the Set Up Project button and complete the steps. You will be prompted about whether you have already defined the configuration file for CircleCI within your project. Enter the branch name (for this tutorial, you are using main).
After the setup is complete, your project will start building. In this case, the build was successfully completed.

Conclusion
Congratulations! If you got this far you have learned how to use and harness the full potential of Playwright by combining both API and UI testing to write efficient and robust tests. This approach not only ensures maintainability of tests but also separation of concerns between the browser and API tests. Just to recap, you learned:
- How to use
APIRequestContextto send requests and why it’s important - How to persist authentication state to bridge the gap between your API and UI tests in Playwright
- How to automate the entire process in CI using CircleCI pipelines.
We hope you enjoyed this tutorial. Until the next one, happy testing!