Intermediate · NoSQL, ORMs and data features · topic 29 of 54 · 25 min
Pagination: sending data page by page
Explanation
Why paginate?
An online shop may have 2 lakh orders. Sending them all in one response would be slow for the database, use lots of server memory and burn the user's mobile data. Nobody reads 2 lakh rows anyway. So APIs return a small page, such as 20 items, and let the app ask for more.
LIMIT and OFFSET
LIMIT says how many rows to return. OFFSET says how many rows to skip first. For page number page (starting at 1) and page size pageSize, the maths is: offset = (page - 1) * pageSize, and totalPages = ceil(total / pageSize).
-- Page 3 with 10 items per page: skip 20, take 10
SELECT id, name, price
FROM products
ORDER BY id
LIMIT 10 OFFSET 20;| page | pageSize | OFFSET | Rows returned (of 45) |
|---|---|---|---|
| 1 | 10 | 0 | 1 to 10 |
| 2 | 10 | 10 | 11 to 20 |
| 5 | 10 | 40 | 41 to 45 (last page, only 5) |
| 6 | 10 | 50 | none |
Cursor (keyset) pagination
OFFSET has a hidden cost: for OFFSET 100000 the database still reads 100000 rows and throws them away. Deep pages get slower and slower. Keyset pagination remembers the last id the client saw and asks for rows after it. With an index on id, the database jumps straight there, so page 5000 is as fast as page 1.
-- The client saw ids up to 1040 on the last page
SELECT id, name, price
FROM products
WHERE id > 1040
ORDER BY id
LIMIT 20;| Offset (page numbers) | Cursor (keyset) | |
|---|---|---|
| Jump to page 7 | Easy | Not possible directly |
| Speed on deep pages | Gets slower | Stays fast |
| New rows added while scrolling | Items can repeat or be skipped | Stable |
| Best for | Admin tables, search results with page numbers | Infinite scroll feeds, big tables, APIs |
What the response should include
// Offset style: GET /api/products?page=2&pageSize=20
{ "items": [ ... ], "page": 2, "pageSize": 20, "total": 134, "totalPages": 7 }
// Cursor style: GET /api/feed?limit=20&cursor=MTA0MA
{ "items": [ ... ], "nextCursor": "MTA2MA", "hasMore": true }The cursor is often the last id encoded as base64, so clients treat it as an opaque string and do not build their own. When there are no more rows, nextCursor is null.
Watch the lesson
Pagination: sending data page by page · 1/6
Big lists must be sent in small pieces. That is pagination.
Prefer a video by a teacher? Find free videos on “offset vs cursor pagination explained” on YouTube ↗
In real life: Infinite scroll on a shopping app
When you scroll the product list on a shopping app, the app first calls GET /api/products?limit=20. The backend returns 20 products and nextCursor. When you reach the bottom, the app sends the cursor back and gets the next 20. Even with lakhs of products and new ones added every minute, each call is fast and you never see the same product twice.
Practice (0/3 done)
Write paginate(items, page, pageSize) that returns { items, page, totalPages }. page starts at 1. items is the slice for that page. totalPages is Math.ceil(items.length / pageSize) (0 for an empty list). A page past the end returns an empty items array.
A stationery shop lists products cheapest first, 3 per page. Write a query that returns page 2: the name and price of the 4th, 5th and 6th cheapest products. Use ORDER BY, LIMIT and OFFSET. Table: products(id, name, price).
Keyset pagination: the app already showed orders up to id 6. Return the next 3 orders (id and amount) after id 6, in id order. Note the ids have gaps, so do not guess with OFFSET. Table: orders(id, amount).
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.