Fake Paginated JSON Generator
Generates JSON with fake pagination structure (page, pageSize, total, items) to mock a REST API.
β
Mock paginated JSON: API design from offset/limit to cursors
Pagination is the most common ergonomic problem in REST and GraphQL API design, and it is also one of the easiest to get wrong. A mock paginated JSON generator lets front-end teams, mobile developers and SDK authors build screens, infinite-scroll feeds and tables before the backend is ready, with response envelopes that match the production contract. It also doubles as a teaching artefact: a single tool that emits offset/limit, cursor, keyset and timestamp variants makes the trade-offs concrete and visible side by side.
The four canonical patterns are easy to recognise. Offset/limit uses ?page=2&limit=20 or ?offset=20&limit=20; simple, jumpable, but unstable when rows are inserted between requests and slow on large tables because the database still has to count and skip the first OFFSET rows. Cursor-based pagination uses an opaque token like ?cursor=eyJpZCI6MTAwfQ==; stable across inserts, fast even on billions of rows, but you cannot jump to "page 50". Keyset (a.k.a. seek) exposes the sort key directly β ?after_id=100&limit=20 β and is what most cursors decode to internally. Timestamp-based, ?since=2024-01-01T00:00:00Z, is the natural fit for activity feeds and webhooks.
Response envelopes and standards
A paginated response is almost always wrapped in an envelope with two parts: a data array of items and a meta object describing the page. Common fields are current_page, per_page, total, total_pages, next_url and prev_url. The JSON:API specification standardises this with a links object containing self, first, last, prev and next URLs β a HATEOAS-style design where the client follows links instead of constructing them. GraphQL Relay uses connection + edges + pageInfo with endCursor and hasNextPage; Stripe returns has_more and an SDK-side auto_paging_iter(); GitHub ships pagination data in an HTTP Link header instead of the body.
{
"data": [{ "id": 21, "name": "Item 21" }],
"meta": {
"current_page": 2,
"per_page": 20,
"total": 137,
"total_pages": 7
},
"links": {
"first": "/items?page=1&limit=20",
"prev": "/items?page=1&limit=20",
"next": "/items?page=3&limit=20",
"last": "/items?page=7&limit=20"
}
}
Cursor pagination in depth
Opaque cursors are usually base64-encoded JSON containing the sort key plus the id of the last item returned, so the server can issue a WHERE (sort_key, id) > (last_sort, last_id) query that hits an index. Opacity matters: if clients learn the format they will tamper with it, and changing the schema later breaks every cached cursor in the wild. Always include a version byte. Cursors are the right default for infinite feeds, activity logs, transactions and any endpoint backed by a table that grows continuously; offset/limit is fine for small admin tables and search results where "page 12" is meaningful.
Page sizes, limits and edge cases
Sensible defaults: default page size 25, max 100, reject anything larger with HTTP 400. Always document the cap. Test the edge cases your front-end will hit: an empty result, a result of exactly limit items (off-by-one in "has next page" logic), the last page, a request beyond the last page (return empty data, not 404), and concurrent inserts (cursor stays stable, offset shifts). A mock generator that lets you set total, per_page and current_page independently is the fastest way to cover all four in component tests.
Frequently asked questions
Cursor or offset? Cursor for infinite feeds, large tables, append-only data and anything performance-sensitive. Offset for small admin lists, search results and any case where the user benefits from jumping to a specific page.
What page size should I use? Default 25, allow 1 to 100, reject anything bigger. Mobile clients usually want 20; data exports want 100.
Which envelope is the standard? There is no single winner. JSON:API is the most prescriptive; Stripe-style (data + has_more) is the most copied by SaaS APIs; GitHub-style Link headers are the cleanest but the least discoverable for SDK consumers.
Do I need a total count? Not always. Returning total requires a second query (SELECT COUNT(*)) that can dominate the request time. For infinite feeds, has_more is enough. For paginated tables that show "1-20 of 137", the count is unavoidable.
Related Tools
Handwriting Generator
Convert typed text into an image with handwriting appearance. Useful for adding a personal touch to digital work.
Resume Generator
Fill a simple printable A4 CV from a form with personal data, education and experience.
Favicon Generator
Generate a favicon from text/emoji in all common sizes (16, 32, 48, 64, 192, 512). PNG download.