REST API
Cursor vs Offset on a Mutating Dataset
RequestValidateMedium
What you will learn
- Demonstrate how offset pagination skips/duplicates rows when the dataset mutates mid-scan
- Use opaque keyset cursors for stable iteration and treat a malformed cursor as a 400
- Choose cursor over offset whenever 'paginate through everything' must be exhaustive and correct
Description
See how offset paging skips/duplicates rows when the dataset changes between pages, and how keyset cursors stay stable.
Test Scenario
// Offset page 2 after inserts overlaps page 1; the opaque cursor does not.
const p1 = await (await request.get('/api/v1/feed?offset=0&limit=10')).json();
const p2 = await (await request.get('/api/v1/feed?offset=10&limit=10&_insert=5')).json();
const overlap = p1.data.filter((a) => p2.data.some((b) => b.id === a.id));
expect(overlap.length).toBeGreaterThan(0); // duplicates โ offset is unsafe under inserts
const bad = await request.get('/api/v1/feed?cursor=not-a-cursor');
expect(bad.status()).toBe(400);Endpoint
/api/v1/feedTesting Tips
- โขThe ?_insert=N knob simulates rows added between page fetches (newest-first feed)
- โขOffset paging shifts after inserts โ the same item appears on two pages (or one is skipped)
- โขKeyset cursor paging (?cursor=) reads id < lastSeen, so inserts above the cursor never disturb it
// Playwright API Testing - RESTimport { test, expect } from '@playwright/test';test('REST API request', async ({ request }) => {const response = await request.get('/api/v1/resource');expect(response.ok()).toBeTruthy();expect(response.status()).toBe(200);const data = await response.json();expect(data).toBeDefined();});
Challenge ID: rest-pagination-drift
2 frameworks available
API Playground
Send requests and inspect responses
Endpoint: /api/v1/feed
Press Enter to send