How to Add Cursor-Based Pagination to a REST API
Replace offset pagination with keyset pagination on a stable, unique sort key for fast, consistent pages. Return opaque base64 cursors and navigation links so clients page reliably.
What and why
Offset pagination (LIMIT 20 OFFSET 1000) gets slow on large tables and can skip or repeat rows when data changes between pages. Cursor (keyset) pagination uses a stable pointer into the result set, so it stays fast and consistent. It is the right default for large or actively changing lists.
Prerequisites
- A REST list endpoint.
- A database with an indexable, ordered key.
- Basic SQL.
Steps
1. Understand offset vs cursor pagination
Offset must scan and discard all skipped rows, so deep pages are expensive. A cursor encodes the last item seen; the next page asks for rows after that item, which the database finds via an index.
2. Choose a stable sort key
The key must be unique and ordered. A monotonic id works; if you sort by a non-unique column like created_at, add the id as a tiebreaker so the order is deterministic.
3. Implement a keyset query
Page forward by comparing against the last seen tuple:
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
For the first page, omit the WHERE clause.
4. Encode and return cursors
Make the cursor opaque so clients do not depend on its structure:
const cursor = Buffer.from(JSON.stringify({ created_at, id })).toString('base64url');
res.json({ data, nextCursor: hasMore ? cursor : null });
Decode it on the next request to get the WHERE parameters.
5. Add navigation links
Return ready-to-use links so clients need not build URLs:
{ "data": [], "links": { "next": "/v1/articles?cursor=eyJ...&limit=20" } }
A null next means the end of the list.
Verification
- Paging through the full list returns each row exactly once.
- Inserting a row mid-pagination does not duplicate or skip results.
- Deep pages respond as fast as the first page.
Next Steps
Support bidirectional paging with a previous cursor, cap the page size to protect the server, and document that cursors are opaque and version-specific so clients never parse them.
Prerequisites
- A REST list endpoint
- A database with an ordered key
- Basic SQL knowledge
Steps
- 1Understand offset vs cursor pagination
- 2Choose a stable sort key
- 3Implement a keyset query
- 4Encode and return cursors
- 5Add navigation links