| name | playwright-api |
| description | API testing skill using Playwright's built-in APIRequestContext for RESTful service validation, authentication flows, and API contract verification. |
| license | MIT |
| metadata | {"author":"thetestingacademy","version":"1.0.0","source":"https://qaskills.sh/skills/thetestingacademy/playwright-api"} |
Playwright API Testing Skill
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.
Core Principles
- Playwright-native API testing -- Use
APIRequestContext instead of external HTTP libraries.
- Type safety -- Define interfaces for all request/response payloads.
- Isolation -- Each test manages its own data lifecycle (create, verify, clean up).
- Comprehensive validation -- Check status codes, headers, response body structure, and timing.
- Reusable abstractions -- Build API client classes for each service domain.
Project Structure
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
Configuration
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',
},
},
});
Response Models
Define TypeScript interfaces for all API payloads:
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: ;
: ;
: ;
?: <, []>;
}
Base API Client
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);
}
(: , : ): <> {
..(, { data });
}
(: , : ): <> {
..(, { data });
}
(: , : ): <> {
..(, { data });
}
(: ): <> {
..();
}
}
Domain-Specific API Client
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}`);
}
(: ): <> {
.(, user);
}
(: , : ): <> {
.(, data);
}
(: ): <> {
.();
}
(: ): <> {
.(, { : query });
}
}
Custom Fixtures
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: 'admin@example.com',
password: 'AdminPass123!',
},
});
const body = response.();
(body.);
},
});
{ expect } ;
Writing API Tests
CRUD Operations
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.).(newUser.);
(body.).();
(body.).();
createdUserId = body.;
});
(, ({ usersApi }) => {
createResponse = usersApi.({
...newUser,
: ,
});
: = createResponse.();
response = usersApi.(created.);
(response.()).();
: = response.();
(body.).(created.);
(body.).(created.);
});
(, ({ usersApi }) => {
createResponse = usersApi.({
...newUser,
: ,
});
: = createResponse.();
response = usersApi.(created., { : });
(response.()).();
: = response.();
(body.).();
});
(, ({ usersApi }) => {
createResponse = usersApi.({
...newUser,
: ,
});
: = createResponse.();
deleteResponse = usersApi.(created.);
(deleteResponse.()).();
getResponse = usersApi.(created.);
(getResponse.()).();
});
});
Authentication Testing
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: 'admin@example.com',
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('admin@example.com');
});
test('should reject invalid credentials', async ({ request }) => {
const response = await request.post('/auth/login', {
data: {
email: 'admin@example.com',
password: ,
},
});
(response.()).();
body = response.();
(body.).();
});
(, ({ request }) => {
loginResponse = request.(, {
: {
: ,
: ,
},
});
{ token } = loginResponse.();
response = request.(, {
: {
: ,
},
});
(response.()).();
user = response.();
(user.).();
});
(, ({ request }) => {
response = request.(, {
: {
: ,
},
});
(response.()).();
});
});
Error Handling and Validation
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).();
});
(, ({ usersApi }) => {
email = ;
userData = { email, : , : };
usersApi.(userData);
response = usersApi.(userData);
(response.()).();
});
(, ({ usersApi }) => {
response = usersApi.();
(response.()).();
});
});
Pagination and Filtering
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, )).();
page1Ids = page1..( u.);
page2Ids = page2..( u.);
overlap = page1Ids.( page2Ids.(id));
(overlap).();
});
});
Response Header Validation
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();
expect(response.headers()['x-content-type-options']).toBe('nosniff');
expect(response.headers()['x-frame-options']).toBe('DENY');
});
Response Time Assertions
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);
});
Best Practices
- Use unique test data -- Include timestamps or UUIDs in emails and names to avoid collisions.
- Clean up after tests -- Delete resources you create to keep the test environment clean.
- Validate response schemas -- Check not just values but the shape of the response.
- Test both happy and unhappy paths -- Always test error cases and edge cases.
- Use environment variables -- Never hardcode URLs or credentials.
- Group tests logically -- Organize by resource or feature, not by HTTP method.
- Use fixtures for authentication -- Avoid repeating login logic in every test.
- Check response times -- API performance is part of correctness.
- Test idempotency -- Verify that repeated identical requests produce consistent results.
- Version your API tests -- When testing versioned APIs, organize tests by version.
Anti-Patterns to Avoid
- Chaining test dependencies -- Each test must create its own data.
- Ignoring response headers -- Headers carry important metadata.
- Testing only status codes -- Always validate the response body too.
- Using hardcoded IDs -- IDs should come from test setup, not hardcoded values.
- Skipping error scenarios -- Error handling tests catch more bugs than happy-path tests.
- Not testing with different roles -- API authorization must be tested per role.
- Mixing UI and API tests -- Keep API tests separate from E2E browser tests.
- Not verifying side effects -- If POST creates a resource, GET it to confirm.
- Ignoring rate limiting -- Test that rate limits are enforced and handle 429 responses.
- Not testing with large payloads -- Ensure APIs handle boundary sizes correctly.
Advanced Patterns
Parallel API Test with Context Isolation
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}`);
});
});
Custom Request Context with Auth
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();
});
File Upload via API
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);
});