API testing skill using Playwright's built-in APIRequestContext for RESTful service validation, authentication flows, and API contract verification.
npx skills add https://github.com/PramodDutta/qaskills --skill playwright-api
You are an expert QA automation engineer specializing in API testing using Playwright's built-in APIRequestContext. When the user asks you to write, review, or debug API tests with Playwright, follow these detailed instructions.
APIRequestContext instead of external HTTP libraries.tests/
api/
auth/
auth-api.spec.ts
users/
users-api.spec.ts
users-crud.spec.ts
products/
products-api.spec.ts
fixtures/
api.fixture.ts
auth-api.fixture.ts
models/
user.model.ts
product.model.ts
api-response.model.ts
clients/
base-api-client.ts
users-api-client.ts
products-api-client.ts
utils/
api-helpers.ts
schema-validator.ts
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/api',
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: [
['html'],
['json', { outputFile: 'test-results/api-results.json' }],
],
use: {
baseURL: process.env.API_BASE_URL || 'http://localhost:3000/api',
extraHTTPHeaders: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
},
});
Define TypeScript interfaces for all API payloads:
// models/user.model.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user' | 'viewer';
createdAt: string;
updatedAt: string;
}
export interface CreateUserRequest {
email: string;
name: string;
password: string;
role?: 'admin' | 'user' | 'viewer';
}
export interface UpdateUserRequest {
name?: string;
role?: 'admin' | 'user' | 'viewer';
}
export interface UserListResponse {
data: User[];
total: number;
page: number;
pageSize: number;
}
export interface ApiError {
statusCode: number;
message: string;
error: string;
details?: Record<string, string[]>;
}
// clients/base-api-client.ts
import { APIRequestContext, APIResponse } from '@playwright/test';
export class BaseApiClient {
protected readonly request: APIRequestContext;
protected readonly basePath: string;
constructor(request: APIRequestContext, basePath: string) {
this.request = request;
this.basePath = basePath;
}
protected async get(path: string, params?: Record<string, string>): Promise<APIResponse> {
const url = params
? `${this.basePath}${path}?${new URLSearchParams(params)}`
: `${this.basePath}${path}`;
return this.request.get(url);
}
protected async post(path: string, data: unknown): Promise<APIResponse> {
return this.request.post(`${this.basePath}${path}`, { data });
}
protected async put(path: string, data: unknown): Promise<APIResponse> {
return this.request.put(`${this.basePath}${path}`, { data });
}
protected async patch(path: string, data: unknown): Promise<APIResponse> {
return this.request.patch(`${this.basePath}${path}`, { data });
}
protected async delete(path: string): Promise<APIResponse> {
return this.request.delete(`${this.basePath}${path}`);
}
}
// clients/users-api-client.ts
import { APIRequestContext, APIResponse } from '@playwright/test';
import { BaseApiClient } from './base-api-client';
import { CreateUserRequest, UpdateUserRequest } from '../models/user.model';
export class UsersApiClient extends BaseApiClient {
constructor(request: APIRequestContext) {
super(request, '/users');
}
async list(page = 1, pageSize = 10): Promise<APIResponse> {
return this.get('', { page: String(page), pageSize: String(pageSize) });
}
async getById(id: string): Promise<APIResponse> {
return this.get(`/${id}`);
}
async create(user: CreateUserRequest): Promise<APIResponse> {
return this.post('', user);
}
async update(id: string, data: UpdateUserRequest): Promise<APIResponse> {
return this.patch(`/${id}`, data);
}
async remove(id: string): Promise<APIResponse> {
return this.delete(`/${id}`);
}
async search(query: string): Promise<APIResponse> {
return this.get('/search', { q: query });
}
}
// fixtures/api.fixture.ts
import { test as base } from '@playwright/test';
import { UsersApiClient } from '../clients/users-api-client';
import { ProductsApiClient } from '../clients/products-api-client';
type ApiFixtures = {
usersApi: UsersApiClient;
productsApi: ProductsApiClient;
authToken: string;
};
export const test = base.extend<ApiFixtures>({
usersApi: async ({ request }, use) => {
await use(new UsersApiClient(request));
},
productsApi: async ({ request }, use) => {
await use(new ProductsApiClient(request));
},
authToken: async ({ request }, use) => {
const response = await request.post('/auth/login', {
data: {
email: '[email protected]',
password: 'AdminPass123!',
},
});
const body = await response.json();
await use(body.token);
},
});
export { expect } from '@playwright/test';
import { test, expect } from '../fixtures/api.fixture';
import { CreateUserRequest, User } from '../models/user.model';
test.describe('Users API - CRUD', () => {
let createdUserId: string;
const newUser: CreateUserRequest = {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
password: 'SecurePass123!',
role: 'user',
};
test('POST /users - should create a new user', async ({ usersApi }) => {
const response = await usersApi.create(newUser);
expect(response.status()).toBe(201);
const body: User = await response.json();
expect(body.id).toBeTruthy();
expect(body.email).toBe(newUser.email);
expect(body.name).toBe(newUser.name);
expect(body.role).toBe('user');
expect(body.createdAt).toBeTruthy();
createdUserId = body.id;
});
test('GET /users/:id - should retrieve the user', async ({ usersApi }) => {
// First create a user
const createResponse = await usersApi.create({
...newUser,
email: `get-test-${Date.now()}@example.com`,
});
const created: User = await createResponse.json();
const response = await usersApi.getById(created.id);
expect(response.status()).toBe(200);
const body: User = await response.json();
expect(body.id).toBe(created.id);
expect(body.email).toBe(created.email);
});
test('PATCH /users/:id - should update the user', async ({ usersApi }) => {
const createResponse = await usersApi.create({
...newUser,
email: `update-test-${Date.now()}@example.com`,
});
const created: User = await createResponse.json();
const response = await usersApi.update(created.id, { name: 'Updated Name' });
expect(response.status()).toBe(200);
const body: User = await response.json();
expect(body.name).toBe('Updated Name');
});
test('DELETE /users/:id - should delete the user', async ({ usersApi }) => {
const createResponse = await usersApi.create({
...newUser,
email: `delete-test-${Date.now()}@example.com`,
});
const created: User = await createResponse.json();
const deleteResponse = await usersApi.remove(created.id);
expect(deleteResponse.status()).toBe(204);
const getResponse = await usersApi.getById(created.id);
expect(getResponse.status()).toBe(404);
});
});
import { test, expect } from '@playwright/test';
test.describe('Authentication API', () => {
test('should login with valid credentials', async ({ request }) => {
const response = await request.post('/auth/login', {
data: {
email: '[email protected]',
password: 'AdminPass123!',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.token).toBeTruthy();
expect(body.expiresIn).toBeGreaterThan(0);
expect(body.user.email).toBe('[email protected]');
});
test('should reject invalid credentials', async ({ request }) => {
const response = await request.post('/auth/login', {
data: {
email: '[email protected]',
password: 'wrongpassword',
},
});
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.message).toBe('Invalid credentials');
});
test('should access protected endpoint with token', async ({ request }) => {
// Login first
const loginResponse = await request.post('/auth/login', {
data: {
email: '[email protected]',
password: 'AdminPass123!',
},
});
const { token } = await loginResponse.json();
// Use the token
const response = await request.get('/users/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.email).toBe('[email protected]');
});
test('should reject expired or invalid token', async ({ request }) => {
const response = await request.get('/users/me', {
headers: {
Authorization: 'Bearer invalid.token.here',
},
});
expect(response.status()).toBe(401);
});
});
test.describe('Users API - Validation', () => {
test('should return 400 for missing required fields', async ({ request }) => {
const response = await request.post('/users', {
data: { name: 'No Email User' },
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.details).toHaveProperty('email');
});
test('should return 400 for invalid email format', async ({ request }) => {
const response = await request.post('/users', {
data: {
email: 'not-an-email',
name: 'Bad Email User',
password: 'SecurePass123!',
},
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.details.email).toContain('must be a valid email');
});
test('should return 409 for duplicate email', async ({ usersApi }) => {
const email = `duplicate-${Date.now()}@example.com`;
const userData = { email, name: 'First', password: 'Pass123!' };
await usersApi.create(userData);
const response = await usersApi.create(userData);
expect(response.status()).toBe(409);
});
test('should return 404 for non-existent resource', async ({ usersApi }) => {
const response = await usersApi.getById('non-existent-id');
expect(response.status()).toBe(404);
});
});
test.describe('Users API - Pagination', () => {
test('should return paginated results', async ({ usersApi }) => {
const response = await usersApi.list(1, 5);
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.data.length).toBeLessThanOrEqual(5);
expect(body.page).toBe(1);
expect(body.pageSize).toBe(5);
expect(body.total).toBeGreaterThanOrEqual(0);
});
test('should return correct page', async ({ usersApi }) => {
const page1 = await (await usersApi.list(1, 2)).json();
const page2 = await (await usersApi.list(2, 2)).json();
const page1Ids = page1.data.map((u: { id: string }) => u.id);
const page2Ids = page2.data.map((u: { id: string }) => u.id);
const overlap = page1Ids.filter((id: string) => page2Ids.includes(id));
expect(overlap).toHaveLength(0);
});
});
test('should return correct response headers', async ({ request }) => {
const response = await request.get('/users');
expect(response.headers()['content-type']).toContain('application/json');
expect(response.headers()['x-request-id']).toBeTruthy();
expect(response.headers()['cache-control']).toBeDefined();
// Security headers
expect(response.headers()['x-content-type-options']).toBe('nosniff');
expect(response.headers()['x-frame-options']).toBe('DENY');
});
test('should respond within acceptable time', async ({ request }) => {
const start = Date.now();
const response = await request.get('/health');
const duration = Date.now() - start;
expect(response.status()).toBe(200);
expect(duration).toBeLessThan(500); // 500ms threshold
});
10. Version your API tests -- When testing versioned APIs, organize tests by version.
10. Not testing with large payloads -- Ensure APIs handle boundary sizes correctly.
test.describe.parallel('Isolated API tests', () => {
test('test A creates and deletes user A', async ({ request }) => {
const res = await request.post('/users', {
data: { email: `a-${Date.now()}@test.com`, name: 'A', password: 'Pass123!' },
});
const user = await res.json();
await request.delete(`/users/${user.id}`);
});
test('test B creates and deletes user B', async ({ request }) => {
const res = await request.post('/users', {
data: { email: `b-${Date.now()}@test.com`, name: 'B', password: 'Pass123!' },
});
const user = await res.json();
await request.delete(`/users/${user.id}`);
});
});
test('admin-only endpoint', async ({ playwright }) => {
const adminContext = await playwright.request.newContext({
baseURL: 'http://localhost:3000/api',
extraHTTPHeaders: {
Authorization: 'Bearer admin-token-here',
},
});
const response = await adminContext.get('/admin/settings');
expect(response.status()).toBe(200);
await adminContext.dispose();
});
import * as fs from 'fs';
import * as path from 'path';
test('should upload a file via API', async ({ request }) => {
const filePath = path.resolve('test-data/sample.pdf');
const fileBuffer = fs.readFileSync(filePath);
const response = await request.post('/files/upload', {
multipart: {
file: {
name: 'sample.pdf',
mimeType: 'application/pdf',
buffer: fileBuffer,
},
description: 'Test upload',
},
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body.filename).toBe('sample.pdf');
expect(body.size).toBeGreaterThan(0);
});
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Run Playwright tests at scale with cloud-hosted browsers and integrated Azure portal reporting.
Browser debugging, performance profiling, and automation via Chrome DevTools MCP. Use when user says "debug this page", "take a screenshot", "check network requests", "profile performance", "inspect console errors", or "analyze page load". Do NOT use for full E2E test suites (use playwright-skill) or non-browser debugging.
QA-test a website or web app and return a 1-5 quality score (5 = flawless, 1 = broken) with evidence. Use when the user wants to test, QA, evaluate, score, or "check how good" a site, page, flow, or app — including a local dev server (e.g. "qa test localhost:5173", "does the checkout work?", "rate this landing page"). Drives a real Browser Use cloud browser, tunneling localhost automatically.
Set up component testing with Playwright using a story gallery — scaffold stories and a gallery dev page driven by the built-in mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue.
Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.
Always use browser-harness for any web interaction: automation, scraping, testing, or site/app work.
Run a browser-based UI review of the WordPress.com Help Center across multiple surfaces, looking for visual and behavioral issues. Use when asked to test the Help Center UI.
Take pramoddutta/playwright-api from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.