A list endpoint must never return everything. There are two ways to page.
1SELECT * FROM tasks ORDER BY created_at DESC LIMIT 20 OFFSET 40; -- page 31const page = 3, pageSize = 20;
2prisma.task.findMany({ orderBy: { createdAt: "desc" }, take: pageSize, skip: (page - 1) * pageSize });Pros: simple, supports "jump to page N", easy total count. Cons: slow on deep pages (the DB still scans and discards the skipped rows), and rows can shift if data changes between page loads (you see a duplicate or miss one).
Instead of "skip 40", you say "give me rows after this one":
1SELECT * FROM tasks
2WHERE created_at < $lastSeenCreatedAt
3ORDER BY created_at DESC
4LIMIT 20;1prisma.task.findMany({
2 take: 20,
3 cursor: { id: lastId },
4 skip: 1, // skip the cursor row itself
5 orderBy: { createdAt: "desc" },
6});Pros: constant speed at any depth, stable under inserts/deletes — ideal for infinite scroll and large datasets. Cons: no "jump to page 50", more complex, total count is awkward.
| Need | Pick |
|---|---|
| Admin table with page numbers | Offset |
| Infinite scroll / feeds | Cursor |
| Millions of rows, deep pages | Cursor |
1{ "data": [...], "meta": { "nextCursor": "eyJpZCI6...", "hasMore": true } }Give clients exactly what they need to fetch the next page — a nextCursor for keyset, or page/total for offset.