| Param | Example |
offset | ?offset=40&limit=20 |
limit | Page size, capped (e.g. 100) |
| Pros | Cons |
| Random page access, simple SQL | Slow for large offsets, inconsistent if data changes |
| Param | Example |
cursor | Opaque token (base64-encoded) |
limit | Page size |
| Response | nextCursor, prevCursor |
Example: Cursor Response
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIzLCJ0cyI6MTcwfQ==",
"hasMore": true
}
}
| Param | Example |
page | ?page=3&perPage=20 |
perPage / size | Items per page |
| Field | Required |
total | Recommended (expensive for large datasets) |
page / perPage | Yes |
totalPages | Computed: ceil(total/perPage) |
hasNext / hasPrev | Optional convenience |
RFC 5988 / 8288 Link header carries navigation links.
Link: </users?page=3&per_page=20>; rel="next",
</users?page=8&per_page=20>; rel="last",
</users?page=1&per_page=20>; rel="first",
</users?page=1&per_page=20>; rel="prev"
6. Handling First and Last Page Links
| rel | Meaning |
first | Page 1 |
prev | Previous page (omit on first) |
next | Next page (omit on last) |
last | Final page (requires total count) |
7. Setting Default and Maximum Page Sizes
| Setting | Typical Value |
| Default page size | 20-25 |
| Maximum page size | 100 (clamp; do not error) |
| Large dataset | Document hard limit, suggest streaming/export |
Uses WHERE (sortKey, id) > (lastSortKey, lastId) instead of OFFSET — O(log n) regardless of page.
Example: Keyset Query
SELECT id, name, created_at FROM users
WHERE (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT 21; -- 21 to detect hasMore
9. Handling Empty Result Sets
| Scenario | Response |
| No results | 200 OK + {"data": [], "total": 0} |
| Page beyond range | 200 OK + empty data (NOT 404) |
| Invalid page param | 400 Bad Request |
| Convention | Recommendation |
| Single style | Pick one (page/cursor/offset) per API |
| Param naming | page+perPage OR limit+offset |
| Across endpoints | Same style for all collections |