Playwright API Testing
This document provides standards for writing API tests in Playwright.
Standard Metadata
- Area: Playwright
- Owner: Technical Leadership group
- Last reviewed: 2026-10-07
- Next review due from: 2026-12-01
- Enforcement: Code review
- Status: Active
Playwright API tests must test only one endpoint · PW-04.1 · MUST · TEST
Each API test must check the behaviour of a single endpoint, so that tests are isolated, failures are easy to diagnose, and the intent of each test is clear. Other endpoints may still be called from hooks to set up or clean up data, as long as the test only asserts against the endpoint under test.
test.describe('GET /customers/{id}', { tag: '@customers' }, () => {
let customerId: number;
// Set-up may use another endpoint
test.beforeEach('Create a customer', async ({ api }) => { ... });
test('Returns the customer with the given ID', { tag: '@high' }, async ({ api }) => {
const getCustomerResponse = await api.customers.getCustomerById(customerId);
expect(getCustomerResponse.status()).toBe(200);
expect(await getCustomerResponse.json()).toMatchObject({ id: customerId, name: Customer.NAME });
});
});
Playwright endpoint files must be named after the endpoint they act on · PW-04.2 · MUST · TEST
// Appointments class within api/endpoints/appointments.ts
export default class Appointments {
request: APIRequestContext;
constructor(request: APIRequestContext) {
this.request = request;
}
endpoint = '/appointments';
By following this naming convention, the tests will document the endpoints that they interact with and API calls will have a short consistent naming convention e.g. await api.appointments.getAppointmentsOnDate(today); .
CRUD actions must be defined as functions in the Playwright endpoint file · PW-04.3 · MUST · TEST
CRUD (create, read, update, delete) functions must be defined in a file relating to the endpoint on which they can be performed (within api/endpoints/), to avoid code duplication.
// Example within api/endpoints/customers.ts
/**
* Perform a GET request on the provided customer ID
* @param {number} id - the ID of the customer to get
* @returns {Promise<APIResponse>} - the response from GET /customers/{id}
*/
public async getCustomerById(id: number): Promise<APIResponse> {
return await this.request.get(this.endpoint + '/' + id, {
headers: { ...await setHeader() },
});
}
Playwright endpoint functions must not take the API request context as a parameter · PW-04.4 · MUST · TEST
Each endpoint class receives the APIRequestContext once, in its constructor, as shown in the endpoint class example. Functions in the class use this.request, so tests only pass the data the request needs.
❌ The test has to supply the request context:
public async getCustomerById(request: APIRequestContext, id: number): Promise<APIResponse> {
return await request.get(this.endpoint + '/' + id);
}
await api.customers.getCustomerById(request, customerId);
✔️ The class already holds the request context:
public async getCustomerById(id: number): Promise<APIResponse> {
return await this.request.get(this.endpoint + '/' + id);
}
await api.customers.getCustomerById(customerId);
Playwright API types must be defined within types/endpoints files · PW-04.5 · MUST · TEST
Define one file per endpoint containing the endpoint’s request and response types.
// Example types/endpoints/upcoming-appointments.ts
// Define the string union for response endpoints
export type ResponseNames =
'SuccessfullyGetUpcomingApts' |
'GetUpcomingAptsWithParams';
export type SuccessfullyGetUpcomingApts =
paths['/upcomingAppointments']['get']['responses']['200']['content']['application/json'];
export type GetUpcomingAptsWithParams =
paths['/upcomingAppointments']['get']['parameters']['query'];
Playwright endpoint functions should use swagger types for request bodies and responses · PW-04.6 · SHOULD · TEST
Inside endpoint functions, use the swagger-generated types from types/endpoints to build the request body from the function’s parameters. Use them again when reading the response. This catches mismatches with the API contract before the tests that rely on this data run.
The function signature itself should still accept individual parameters.
// api/endpoints/customers.ts
public async createCustomer(name: string, houseNo: number, postcode: string): Promise<APIResponse> {
// The swagger type checks the request body matches the API contract
const body: ApiTypes.Customers.CreateCustomerBody = { name, houseNo, postcode };
return await this.request.post(this.endpoint, { data: body, headers: { ...await setHeader() } });
}
// tests/api-tests/customers/customers.post.spec.ts
const createCustomerResponse = await api.customers.createCustomer(name, houseNo, postcode);
const createdCustomer: ApiTypes.Customers.SuccessfullyCreateCustomer = await createCustomerResponse.json();
API type naming conventions · PW-04.7
Playwright API request types should end in Body or WithParams and use no outcome words · PW-04.7.1 · SHOULD · TEST
Name request body types ending in Body, and query or path parameter types ending in WithParams:
| API type | Naming | Example |
|---|---|---|
| Request body | action + entity + Body |
CreateUpcomingAptBody |
| Query or path parameters | action + entity + WithParams |
GetTemplatesWithParams |
Do not use outcome words such as “valid”, “invalid”, “successful” or “unsuccessful” in request type names. A request type only describes the shape of the data sent, not the outcome. For example, the valid template ID 30 and the invalid template ID 987 are both numbers, so they share the same type, and only the response shows which one succeeded.
// Example requestBody API type
export type CreateUpcomingAptBody =
paths['/upcomingAppointments']['post']['requestBody']['content']['text/json'];
// Example GET request with parameters API type
export type GetTemplatesWithParams =
paths['/templates']['get']['parameters']['query'];
Playwright API response types must be named after their HTTP status · PW-04.7.2 · Must · TEST
Start response type names with the meaning of their status, such as Successfully for a 200 response or Unauthorized for a 401 response. Each response type is taken from a specific status in the swagger definition, so its name should say which one.
| API type | Naming | Example |
|---|---|---|
| Response | status meaning + action or entity | SuccessfullyGetAppointments (200), UnauthorizedCustomer (401) |
// Example 200 response API type
export type SuccessfullyGetAppointments =
paths['/upcomingAppointments']['get']['responses']['200']['content']['application/json'];
// Example 401 response API type
export type UnauthorizedCustomer =
paths['/upcomingAppointments']['get']['responses']['401']['content']['application/json'];
Playwright endpoint functions must accept individual parameters · PW-04.8 · MUST · TEST
By accepting individual parameters to functions, it is easier to spot type errors and refactor code should the underlying API structure get updated.
JSDocs are also able to provide type information at the point the function is being called, for example the expected parameter, id is a number.

Contrastingly, using the API type taken from swagger:
export type CreateCustomerBody = paths['/customers']['post']['requestBody']['content']['application/json'];

Here the JSDoc is unable to determine the format of the required request body from types/endpoints/customers.ts and selecting this file does not reveal the underlying JSON object structure either.
✔️ Good Example Test File
// Define the data required in the test
const name: string = faker.company.name();
const houseNo: number = faker.number.int({ min: 1, max: 300 });
const postcode: string = randomPostcodeString();
// Perform the API request and assert against its result
const createCustomerResponse = await api.customers.createCustomer(name, houseNo, postcode);
expect(createCustomerResponse.status()).toBe(201);
// toMatchObject checks every property sent, while ignoring the generated id
expect(await createCustomerResponse.json()).toMatchObject({ name, houseNo, postcode });
❌ Bad Example Test File
// Define the API request body or parameters within the test
const addCustomerRequest: ApiTypes.Customers.CreateCustomerBody = {
id: null,
name,
houseNo,
postcode,
};
// Provide the request body to the function which will perform the POST request
const createCustomerResponse = await api.customers.createCustomer(addCustomerRequest);
In this example, should the property houseNo get renamed, every test which calls the createCustomer function would have to be updated.
It is also harder to assert against the outcome as either; the ID would have to get stripped from the response before the objects can be compared, or each individual element would have to be asserted against.
Playwright endpoint functions should be named after the business action, not the HTTP method · PW-04.9 · SHOULD · TEST
Name endpoint functions after what they do in the system, not after the HTTP method they send. The same method can perform several different actions on one endpoint. For example, a PUT might edit a customer’s details or add pets to a customer, so a name like putCustomer() does not tell the reader which one.
| ❌ Bad Example | ✔️ Good Example |
|---|---|
postCustomer(name: string, houseNo: number, postcode: string) |
createCustomer(name: string, houseNo: number, postcode: string) |
putCustomer(id: number, name: string, houseNo: number, postcode: string) |
editCustomer(id: number, name: string, houseNo: number, postcode: string) |
putCustomerPets(id: number, petName: string, petType: AnimalId, petBreed: DogBreedId | CatBreedId | string) |
addPetDetailsToCustomer(id: number, petName: string, petType: AnimalId, petBreed: DogBreedId | CatBreedId | string) |