# Playwright API Testing · PW-04

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.

```ts
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**

```ts
// 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.

```ts
// 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](#playwright-endpoint-files-must-be-named-after-the-endpoint-they-act-on). Functions in the class use `this.request`, so tests only pass the data the request needs.

❌ The test has to supply the request context:

```ts
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:

```ts
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.

```ts
// 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](#playwright-endpoint-functions-must-accept-individual-parameters).

```ts
// 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.

```ts
// 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) |

```ts
// 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.

![JSDoc showing how the type of individual parameters can be previewed in VSCode](/standards-assets/resources/id-param-JSDoc.png)

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

![JSDoc showing the requestBody type points to the paths file](/standards-assets/resources/request-body-JSDoc.png)

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

```ts
// 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

```ts
// 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)` |
