# API Request Utility ## Principle Use typed HTTP client with built-in schema validation and automatic retry for server errors. The utility handles URL resolution, header management, response parsing, and single-line response validation with proper TypeScript support. ## Rationale Vanilla Playwright's request API requires boilerplate for common patterns: - Manual JSON parsing (`await response.json()`) - Repetitive status code checking - No built-in retry logic for transient failures - No schema validation - Complex URL construction The `apiRequest` utility provides: - **Automatic JSON parsing**: Response body pre-parsed - **Built-in retry**: 5xx errors retry with exponential backoff - **Schema validation**: Single-line validation (JSON Schema, Zod, OpenAPI) - **URL resolution**: Four-tier strategy (explicit > config > Playwright > direct) - **TypeScript generics**: Type-safe response bodies ## Pattern Examples ### Example 1: Basic API Request **Context**: Making authenticated API requests with automatic retry and type safety. **Implementation**: ```typescript import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; test('should fetch user data', async ({ apiRequest }) => { const { status, body } = await apiRequest({ method: 'GET', path: '/api/users/123', headers: { Authorization: 'Bearer token' }, }); expect(status).toBe(200); expect(body.name).toBe('John Doe'); // TypeScript knows body is User }); ``` **Key Points**: - Generic type `` provides TypeScript autocomplete for `body` - Status and body destructured from response - Headers passed as object - Automatic retry for 5xx errors (configurable) ### Example 2: Schema Validation (Single Line) **Context**: Validate API responses match expected schema with single-line syntax. **Implementation**: ```typescript import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; test('should validate response schema', async ({ apiRequest }) => { // JSON Schema validation const response = await apiRequest({ method: 'GET', path: '/api/users/123', validateSchema: { type: 'object', required: ['id', 'name', 'email'], properties: { id: { type: 'string' }, name: { type: 'string' }, email: { type: 'string', format: 'email' }, }, }, }); // Throws if schema validation fails // Zod schema validation import { z } from 'zod'; const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), }); const response = await apiRequest({ method: 'GET', path: '/api/users/123', validateSchema: UserSchema, }); // Response body is type-safe AND validated }); ``` **Key Points**: - Single `validateSchema` parameter - Supports JSON Schema, Zod, YAML files, OpenAPI specs - Throws on validation failure with detailed errors - Zero boilerplate validation code ### Example 3: POST with Body and Retry Configuration **Context**: Creating resources with custom retry behavior for error testing. **Implementation**: ```typescript test('should create user', async ({ apiRequest }) => { const newUser = { name: 'Jane Doe', email: 'jane@example.com', }; const { status, body } = await apiRequest({ method: 'POST', path: '/api/users', body: newUser, // Automatically sent as JSON headers: { Authorization: 'Bearer token' }, }); expect(status).toBe(201); expect(body.id).toBeDefined(); }); // Disable retry for error testing test('should handle 500 errors', async ({ apiRequest }) => { await expect( apiRequest({ method: 'GET', path: '/api/error', retryConfig: { maxRetries: 0 }, // Disable retry }), ).rejects.toThrow('Request failed with status 500'); }); ``` **Key Points**: - `body` parameter auto-serializes to JSON - Default retry: 5xx errors, 3 retries, exponential backoff - Disable retry with `retryConfig: { maxRetries: 0 }` - Only 5xx errors retry (4xx errors fail immediately) ### Example 4: URL Resolution Strategy **Context**: Flexible URL handling for different environments and test contexts. **Implementation**: ```typescript // Strategy 1: Explicit baseUrl (highest priority) await apiRequest({ method: 'GET', path: '/users', baseUrl: 'https://api.example.com', // Uses https://api.example.com/users }); // Strategy 2: Config baseURL (from fixture) import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; test.use({ configBaseUrl: 'https://staging-api.example.com' }); test('uses config baseURL', async ({ apiRequest }) => { await apiRequest({ method: 'GET', path: '/users', // Uses https://staging-api.example.com/users }); }); // Strategy 3: Playwright baseURL (from playwright.config.ts) // playwright.config.ts export default defineConfig({ use: { baseURL: 'https://api.example.com', }, }); test('uses Playwright baseURL', async ({ apiRequest }) => { await apiRequest({ method: 'GET', path: '/users', // Uses https://api.example.com/users }); }); // Strategy 4: Direct path (full URL) await apiRequest({ method: 'GET', path: 'https://api.example.com/users', // Full URL works too }); ``` **Key Points**: - Four-tier resolution: explicit > config > Playwright > direct - Trailing slashes normalized automatically - Environment-specific baseUrl easy to configure ### Example 5: Integration with Recurse (Polling) **Context**: Waiting for async operations to complete (background jobs, eventual consistency). **Implementation**: ```typescript import { test } from '@seontechnologies/playwright-utils/fixtures'; test('should poll until job completes', async ({ apiRequest, recurse }) => { // Create job const { body } = await apiRequest({ method: 'POST', path: '/api/jobs', body: { type: 'export' }, }); const jobId = body.id; // Poll until ready const completedJob = await recurse( () => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }), (response) => response.body.status === 'completed', { timeout: 60000, interval: 2000 }, ); expect(completedJob.body.result).toBeDefined(); }); ``` **Key Points**: - `apiRequest` returns full response object - `recurse` polls until predicate returns true - Composable utilities work together seamlessly ## Comparison with Vanilla Playwright | Vanilla Playwright | playwright-utils apiRequest | | ---------------------------------------------- | ---------------------------------------------------------------------------------- | | `const resp = await request.get('/api/users')` | `const { status, body } = await apiRequest({ method: 'GET', path: '/api/users' })` | | `const body = await resp.json()` | Response already parsed | | `expect(resp.ok()).toBeTruthy()` | Status code directly accessible | | No retry logic | Auto-retry 5xx errors with backoff | | No schema validation | Built-in multi-format validation | | Manual error handling | Descriptive error messages | ## When to Use **Use apiRequest for:** - ✅ API endpoint testing - ✅ Background API calls in UI tests - ✅ Schema validation needs - ✅ Tests requiring retry logic - ✅ Typed API responses **Stick with vanilla Playwright for:** - Simple one-off requests where utility overhead isn't worth it - Testing Playwright's native features specifically - Legacy tests where migration isn't justified ## Related Fragments - `overview.md` - Installation and design principles - `auth-session.md` - Authentication token management - `recurse.md` - Polling for async operations - `fixtures-composition.md` - Combining utilities with mergeTests - `log.md` - Logging API requests ## Anti-Patterns **❌ Ignoring retry failures:** ```typescript try { await apiRequest({ method: 'GET', path: '/api/unstable' }); } catch { // Silent failure - loses retry information } ``` **✅ Let retries happen, handle final failure:** ```typescript await expect(apiRequest({ method: 'GET', path: '/api/unstable' })).rejects.toThrow(); // Retries happen automatically, then final error caught ``` **❌ Disabling TypeScript benefits:** ```typescript const response: any = await apiRequest({ method: 'GET', path: '/users' }); ``` **✅ Use generic types:** ```typescript const { body } = await apiRequest({ method: 'GET', path: '/users' }); // body is typed as User[] ```