Implementing API Pagination
1. Implementing Offset-Based Pagination
Example: Offset query
@GetMapping("/orders")
public PageResponse<OrderResponse> list(
@RequestParam(defaultValue="0") int page,
@RequestParam(defaultValue="20") int size) {
var pg = PageRequest.of(page, size, Sort.by("placedAt").descending());
return mapper.toResponse(orderRepo.findAll(pg));
}
Warning: Offset pagination becomes O(N) at the DB; avoid for large datasets — use cursor.
2. Implementing Cursor-Based Pagination
Example: Keyset cursor
-- cursor encodes (placed_at, id) of last item
SELECT * FROM orders
WHERE (placed_at, id) < (?, ?)
ORDER BY placed_at DESC, id DESC
LIMIT 21; -- 1 extra to detect hasNext
3. Designing Page Response Format
| Field | Purpose |
|---|---|
| items | Page contents |
| page, size | Offset metadata |
| totalElements, totalPages | If known |
| nextCursor, prevCursor | Cursor metadata |
| hasNext, hasPrev | Convenience flags |
4. Implementing Total Count Calculation
| Approach | Trade-off |
|---|---|
| Exact COUNT(*) | Slow on big tables |
| Estimated | pg_class.reltuples (PG) |
| Skip total | Cursor pagination |
| Async total | Compute separately, cache |
5. Handling Empty Results
Example: Empty page
{ "items": [], "page": 0, "size": 20, "totalElements": 0,
"totalPages": 0, "hasNext": false, "hasPrev": false }
6. Designing Default Page Sizes
| Setting | Recommendation |
|---|---|
| Default size | 20 |
| Max size | 100 (cap to prevent abuse) |
| Validate | Reject > max with 400 |
7. Implementing Sorting with Pagination
| Aspect | Detail |
|---|---|
| Format | ?sort=field,direction |
| Multiple | ?sort=status,asc&sort=placedAt,desc |
| Allow-list | Whitelist sortable fields |
| Stable sort | Append PK as tie-breaker |
8. Using HATEOAS Links
Example: Page links
"_links": {
"self": { "href": "/orders?page=2&size=20" },
"first": { "href": "/orders?page=0&size=20" },
"prev": { "href": "/orders?page=1&size=20" },
"next": { "href": "/orders?page=3&size=20" },
"last": { "href": "/orders?page=99&size=20" }
}
9. Handling Large Datasets
| Strategy | Detail |
|---|---|
| Cursor pagination | O(1) per page |
| Streaming | NDJSON / chunked transfer |
| Async export | Generate file, notify when done |
| Read replica | Offload analytics queries |
10. Implementing Search with Pagination
| Concern | Detail |
|---|---|
| Index sort | Match search engine sort order |
| Result freshness | Document new results between pages |
| Scroll API | Elasticsearch search_after |
11. Implementing Pagination Metadata
| Header | Purpose |
|---|---|
X-Total-Count | Total items |
Link | RFC 5988 next/prev/first/last |
X-Page, X-Page-Size | Current page info |
12. Implementing Consistent Pagination
| Issue | Mitigation |
|---|---|
| Inserts shift offset | Use cursor, not offset |
| Sort field collisions | Tie-break on PK |
| Deleted items | Snapshot or filter at query time |