Sitemap
Roshan Tiwari

Roshan Tiwari

Playwright API Automation

3 min readJun 30, 2026

--

API Testing with Playwright (TypeScript) — CRUD Guide

API testing doesn’t need a heavyweight framework. Playwright ships a built-in request fixture that can fire GET, POST, PUT, PATCH, and DELETE calls, assert on status codes, and validate response bodies — all without spinning up a browser. Below is a complete, beginner-friendly CRUD walkthrough using the dummy test API (https://test.in/public/v2/users), followed by an interview prep section.

Why Playwright for API Testing?

  • No browser overhead — request context works standalone, making tests fast.
  • Built-in assertions — expect(response.status()) reads like plain English.
  • One framework, two jobs — the same Playwright project can run UI and API tests together.
  • First-class TypeScript support — strong typing catches mistakes before runtime.

Setup

npm init playwright@latest

This scaffolds a project with a tests/ folder and playwright.config.ts. Every test below uses the request fixture injected by Playwright Test.

1. GET — Read (List Users)

What it does: Fetches all users and confirms the API responded successfully with data.

import { test, expect } from '@playwright/test';
test('GET - should fetch list of users', async ({ request }) => {
const response = await request.get('https://test.in/public/v2/users');
expect(response.status()).toBe(200); const responseBody = await response.json();
expect(responseBody.length).toBeGreaterThan(0);
});

In short: GET is read-only and idempotent — calling it repeatedly should never change server state. Always assert both the status code and that the payload looks like real data.

2. POST — Create

What it does: Creates a brand-new user using a Bearer token and a unique email (timestamp avoids duplicate-email failures).

test('POST - should create a new user', async ({ request }) => {
const response = await request.post('https://test.in/public/v2/users', {
headers: {
Authorization: 'Bearer demo-token',
'Content-Type': 'application/json',
},
data: {
name: 'Testab123',
email: `testab123_${Date.now()}@example.com`,
gender: 'M',
status: 'active',
},
});
  expect(response.status()).toBe(201);  const responseBody = await response.json();
expect(responseBody.name).toBe('Testab123');
});

In short: POST creates a resource and should return 201 Created, plus the created object in the response body — always confirm the data you sent matches what came back.

Get Roshan Tiwari’s stories in your inbox

Join Medium for free to get updates from this writer.

3. PUT — Full Update

What it does: Replaces every field of an existing user (ID 1032 here). Any field you omit may get wiped/reset, since PUT expects the complete object.

test('PUT - should fully update a user', async ({ request }) => {
const response = await request.put('https://test.in/public/v2/users/1032', {
headers: {
Authorization: 'Bearer demo-token',
'Content-Type': 'application/json',
},
data: {
name: 'Testab123',
email: `testab123_updated_${Date.now()}@example.com`,
gender: 'M',
status: 'active',
},
});
  expect(response.status()).toBe(200);
});

In short: PUT = full replace. Send the complete payload, not a partial one, or you risk losing existing field values.

4. PATCH — Partial Update

What it does: Updates only the status field, leaving everything else untouched.

test('PATCH - should partially update a user', async ({ request }) => {
const response = await request.patch('https://test.in/public/v2/users/1032', {
headers: {
Authorization: 'Bearer demo-token',
'Content-Type': 'application/json',
},
data: {
status: 'inactive',
},
});
  expect(response.status()).toBe(200);  const responseBody = await response.json();
expect(responseBody.status).toBe('inactive');
});

In short: PATCH = surgical update. Send only the fields that change — it’s lighter and safer than PUT for small edits.

5. DELETE — Remove

What it does: Deletes a user by ID and confirms the server returned no content.

test('DELETE - should delete a user', async ({ request }) => {
const response = await request.delete('https://test.in/public/v2/users/1031', {
headers: {
Authorization: 'Bearer demo-token',
},
});
  expect(response.status()).toBe(204);
});

In short: A successful DELETE typically returns 204 No Content — there’s nothing to validate in the body, only the status code.

6. GET with Query Params — Filtering

What it does: Fetches only users whose status equals inactive using query parameters instead of manually building a URL string.

test('GET - should filter users by status', async ({ request }) => {
const response = await request.get('https://test.in/public/v2/users', {
params: { status: 'inactive' },
});
  expect(response.status()).toBe(200);  const responseBody = await response.json();
expect(responseBody.length).toBeGreaterThan(0);
});

In short: The params option keeps query strings type-safe and readable instead of string-concatenating ?status=inactive by hand.

Summary

This suite covers all major CRUD operations and query-parameter handling using Playwright’s built-in request API, with clear assertions for status codes and response bodies — a solid, interview-ready foundation for API test automation.

--

--