API, Data & Developer Tools

API Pagination Test Data Builder

Generate page, offset and cursor pagination fixtures with boundary cases - empty pages, last page, duplicated and missing items under concurrent writes - plus the assertions to test them.

  • Page responses JSON
  • Expected assertions
  • Scenario explanation
Runs in your browser

Everything you paste, type or drop is processed in this browser tab. It is not uploaded, logged, stored or sent to analytics.

Pagination fixtures workspace

1 Pagination style

Scenarios:
Style

0 to 1,000. Try 0, exactly one page, and one more than a page.

2 Writes while the client pages

3 Fixtures and assertions

What the API Pagination Test Data Builder does

This builder produces the responses a paginated API should return for page-number, offset and cursor pagination - including the empty collection, the partial last page and the request past the end - and simulates rows being created or deleted while a client is paging, so you can see exactly which items come back twice or not at all.

Pagination bugs rarely show up in a test that reads a static list. They appear in production, when a new order arrives between page 1 and page 2 and the customer sees the same row twice, or a deletion makes a row vanish from an export. The fixtures here make those cases concrete and checkable.

How to use it

  1. Choose a style: page number (?page=2&per_page=10), offset (?offset=10&limit=10) or cursor (?limit=10&cursor=...).
  2. Set the page size and how many items the collection holds. Try the boundaries: 0 items, exactly one page, and one item more than a page.
  3. Pick a scenario for writes during paging - new rows created, or rows the client has already seen deleted - and after which request it happens.
  4. Read the Responses tab for each request and response body, and the Assertions tab for what holds and what is violated in these fixtures.
  5. Download the fixtures as JSON for a stub server or contract test, or copy the Markdown into a test plan or bug report.

Reading the results

Items are sorted newest first (id descending), the order most feeds and admin lists use - and the one where concurrent inserts do the most damage to offset pagination.

Served twice and Never served are counted from what the simulated client actually received, not predicted. An item deleted during paging, or created after paging began, is not counted as missing: it did not exist for the whole traversal.

In the Assertions tab, a violated line is the defect the scenario demonstrates. Your API's own tests should assert the healthy version: for a cursor API, no duplicates and no gaps under concurrent writes; for an offset API, either accept and document the limitation or move to keyset pagination.

Worked example: 25 items, 10 per page, two new rows after the first request

With offset pagination the first request returns ids 25 to 16. Two new items, 26 and 27, are then created and sort to the top. The second request, offset=10, now starts two rows earlier than intended and returns ids 17 to 8, so ids 17 and 16 are served twice. The third request returns 7 to 1, and a request at offset=30 returns an empty list. That is 27 items served, 25 unique, 2 duplicates.

Switch the scenario to deleted rows: after page 1, the client's first two rows (25 and 24) are deleted, the rest move up, and page 2 returns 13 to 4 - ids 15 and 14 are never served, although they existed the whole time.

With cursor pagination the second request asks for ids below 16 (the cursor eyJpZCI6MTZ9 is base64url for {"id":16}), so neither the inserts nor the deletions move it: 25 items, no duplicates, no gaps, and next_cursor is null on the third and last page.

Offset versus cursor pagination

Offset and page-number pagination say how many rows to skip. They are simple, allow jumping to page 40, and give a total count - but the count is recalculated on every request, so any insert or delete before the current position shifts everything after it.

Cursor (keyset) pagination says where to continue from, using the sort key of the last row served (WHERE id < 16 ORDER BY id DESC LIMIT 10). It is stable under concurrent writes and stays fast on large tables because the database seeks rather than skips, but it cannot jump to an arbitrary page and the sort key must be unique - add the id as a tie-breaker when sorting by a timestamp.

Boundary cases worth a test each

An empty collection (one empty page, total 0 or next_cursor null); a collection of exactly one page (no second request, or an empty one); one item more than a page (a last page of one item); a request past the end (an empty data array, not a 404 or 500); a page size of 1; and a cursor that has been tampered with, which the server should reject with 400 rather than crash on.

Limitations: what the result does not prove

  • It simulates one client paging one list. Real systems add isolation levels, replication lag and caches, which can produce other anomalies than the two modelled here.
  • The cursor is the last id encoded in base64url for readability. A production cursor should be opaque to clients and ideally signed or encrypted so it cannot be forged to reach other rows.
  • Response field names (data, per_page, next_cursor and so on) follow common conventions, not a standard; rename them to match your API before using the fixtures.
  • Collections are capped at 1,000 items and 100 per page, which is plenty to show every behaviour.

Privacy: where your data goes

Everything you paste, type or drop is processed in this browser tab. It is not uploaded, logged, stored or sent to analytics. Session recording and tag-manager scripts are switched off on this page.

Standards and sources

Frequently asked questions

Why does offset pagination return duplicate items?

Because the offset counts rows at the moment of each request. If rows are inserted ahead of the client's position between two requests, everything shifts down and the next page starts on rows the client already has. The Offset + new rows scenario shows the exact ids.

Why do some rows go missing when paginating an export?

Rows the client has already read were deleted, so every later row moved up by that many positions and the next offset skipped past them. It is the mirror image of the duplicate problem, and keyset pagination avoids both.

What should an API return for a page past the end?

A normal successful response with an empty data array, and metadata that says there is nothing more (has_more false or next_cursor null). Returning 404 makes clients treat the end of a list as an error, and returning 500 is a bug.

How do I make cursor pagination work when sorting by created_at?

Timestamps are not unique, so use a compound key: sort by created_at and then id, and encode both in the cursor. The next page asks for rows where (created_at, id) is less than the last pair, which is stable even when many rows share a timestamp.

Can I use these fixtures in a contract test?

Yes. The JSON download contains each request line, the full response body and the assertions with their results. Feed the responses to a stub server and check that your client stops at the right page, handles the empty last request and de-duplicates if it must.

Should I return a total count with cursor pagination?

Only if clients need it and you can afford it. Counting a large filtered table on every request is expensive and the number is out of date as soon as rows change. Many APIs return has_more instead and offer the count on a separate, cacheable call.

Last reviewed by the A2Z.Tools team against the sources listed above.

Rate this tool

Was this tool useful? Your feedback helps us improve it.

No ratings yet — be the first to rate this tool.
Your rating (required)
0 / 2000

Please do not include passwords, payment details or other sensitive information.

Your feedback is sent privately to the A2Z.Tools team and will not be posted publicly.