API Testing
When to Use
Verifying HTTP responses directly, or setting up state via API + verifying via UI (the killer pattern).
Pattern: The request Fixture
test('order API', async ({ request }) => {
const get = await request.get('/api/orders/42');
expect(get.ok()).toBeTruthy(); // 200–299
expect(get.status()).toBe(200);
expect(get.headers()['content-type']).toContain('application/json');
const body = await get.json();
expect(body.total).toBe(123.45);
const post = await request.post('/api/orders', {
data: { items: [{ sku: 'X', qty: 2 }] }, // auto-serializes to JSON
headers: { Authorization: 'Bearer …' },
});
await expect(post).toBeOK(); // web-first variant
await request.put('/api/orders/42', { data: { status: 'paid' } });
await request.delete('/api/orders/42');
// Multipart
await request.post('/api/upload', {
multipart: {
file: { name: 'a.png', mimeType: 'image/png', buffer: Buffer.from('...') },
caption: 'Hello',
},
});
});
request reads baseURL, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors, and proxy from use: — same knobs as the browser context.
Pattern: Cookie Sharing Between API and UI
The request fixture and the page fixture share their BrowserContext's cookie jar:
test('login via API, drive UI as authed user', async ({ request, page }) => {
// Login via API — Set-Cookie ends up in the shared jar
const res = await request.post('/user/login?_format=json', {
data: { name: 'admin', pass: 'admin' },
});
await expect(res).toBeOK();
// page is now authenticated
await page.goto('/admin');
await expect(page.getByRole('heading', { name: 'Administration' })).toBeVisible();
});
Pattern: UI + API Hybrid (the Killer Pattern)
test('publishing a node updates JSON:API', async ({ request, page }) => {
// 1. Set up state via API (fast)
const created = await request.post('/jsonapi/node/article', { /* ... */ });
const nuid = (await created.json()).data.id;
// 2. Drive UI for the actual feature under test
await page.goto(`/node/${nuid}/edit`);
await page.getByLabel('Published').check();
await page.getByRole('button', { name: 'Save' }).click();
// 3. Verify via API (cheap, deterministic)
const verify = await request.get(`/jsonapi/node/article/${nuid}`);
const body = await verify.json();
expect(body.data.attributes.status).toBe(true);
});
The most under-used Playwright pattern. Keeps the UI surface tiny while asserting against authoritative state.
Decision: API vs UI E2E vs Hybrid
| Test value | Approach |
|---|---|
| HTTP contract (status, JSON shape, headers) | Pure API — no browser; 10–100× faster |
| User journey (forms, JS-rendered components, navigation) | UI-only E2E |
| Both: verify a feature behaves end-to-end with deterministic state | API + UI hybrid |
Common Mistakes
- Using UI for state setup that could be done via API — slow tests
- Asserting only via UI when API verification would be more authoritative
- Spinning up
requestfor every assertion instead of using the shared fixture
See Also
- Drupal & DDEV Patterns — JSON:API + UI hybrid applied to Drupal entity CRUD
- Network Mocking — intercepting API calls for determinism
- Reference: Playwright API Testing