Tech corner - 17. September 2025

API testing made simple with Playwright

header_image

Playwright is well-known for browser automation, but it’s also a powerful tool for API testing. By building a simple client class with getters and setters, you can keep your tests readable, maintainable, and DRY (Don’t Repeat Yourself).

Why use getters & setters?

  1. Getters: Dynamically compute values like headers, URLs, or tokens so your methods always use the latest state.
  2. Setters: Update values like authentication tokens or tenant IDs, automatically affecting how future requests behave.

This pattern centralizes state and configuration, allowing each test to focus on what it’s actually validating — not on setup details.

Basic API client example

typescript

>// apiClient.ts
>import { APIRequestContext } from '@playwright/test';
>

>export class ApiClient {
>constructor(private request: APIRequestContext) {}
>

>private _token?: string;
>set token(value: string | undefined) {
>this._token = value;
>}
>private _tenant?: string;
>set tenant(value: string | undefined) {
>this._tenant = value;
>}
>get headers() {
>return {
>...(this._token ? { Authorization: `Bearer ${this._token}` } : {}),
>...(this._tenant ? { 'X-Tenant-ID': this._tenant } : {}),
>Accept: 'application/json'
>};
>}
>get baseUrl() {
>return process.env.API_BASE ?? 'https://sandbox.example.com';
>}
>

>async getUser(id: string) {
>const resp = await this.request.get(`${this.baseUrl}/v1/users/${id}`, { headers: this.headers });
>return resp.json();
>}
>async createUser(email: string) {
>const resp = await this.request.post(`${this.baseUrl}/v1/users`, {
>headers: this.headers,
>data: { email }
>});
>return resp.json();
>}
>async deleteUser(id: string) {
>const resp = await this.request.delete(`${this.baseUrl}/v1/users/${id}`, { headers: this.headers });
>return resp.status() === 204;
>}
>// Add more methods as needed!
>}
>

Example test file

typescript

>// user.spec.ts
>import { test, expect } from '@playwright/test';
>import { ApiClient } from './apiClient';
>

>test('create, fetch, and delete a user', async ({ request }) => {
>const client = new ApiClient(request);
>client.token = process.env.API_TOKEN; // Set once for all requests
>client.tenant = 'tenantA'; // If needed
>

>const email = `user${Date.now()}@test.com`;
>const created = await client.createUser(email);
>expect(created.email).toBe(email);
>

>const fetched = await client.getUser(created.id);
>expect(fetched.id).toBe(created.id);
>

>const deleted = await client.deleteUser(created.id);
>expect(deleted).toBeTruthy();
>});
>

Benefits

  1. No duplicate code: Base URL, headers, and token logic are all in one place.
  2. Easy maintenance: Update tokens, headers, or add endpoints with minimal changes.
  3. Scalable: Add new endpoints or features (pagination, retries, schema validation) as your API grows.
  4. Test clarity: Your test files show only business logic, not setup noise.

Extending the client

Want to check response shapes or add error handling? Add helper methods:

typescript

>async assertUserShape(obj: any) {
>for (const k of ['id', 'email']) {
>if (!(k in obj)) throw new Error('Missing key: ' + k);
>}
>}
>

Then use in tests:

typescript

>const user = await client.getUser('123');
>await client.assertUserShape(user);
>

TL;DR

Getters and setters make Playwright API tests clean, scalable, and easy to maintain.

Set your token and tenant once, call intuitive client methods, and keep your test files focused on what really matters.

blog author
Author
Matus Hajdu

I’m an automation tester who professionally breaks things so users don’t—then stitches quality back together with scripts, sneaky assertions, and an irresponsible amount of coffee. Off the clock, I’m a family-first logistics coordinator and an amateur hockey puck magnet, still chasing preseason glory while timing how fast I can lace skates before someone needs a beer.

Read more

Contact us

Let's talk

I hereby consent to the processing of my personal data.