seed-skills/playwright-api/SKILL.md
API testing skill using Playwright's built-in APIRequestContext for RESTful service validation, authentication flows, and API contract verification.
npx skillsauth add PramodDutta/qaskills Playwright API TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
});
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);
});
development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.