Common Pagination Mistakes in Node.js (and How to Fix Them)
Pagination has predictable failure modes. Here are common mistakes and fixes.
Common Pagination Mistakes in Node.js
Pagination has predictable failure modes. Here are common mistakes and how to fix them.
Mistake 1: No Pagination
You do User.find() and return every user. Fix: always paginate. Even if the collection is small today, it will grow.
Mistake 2: No Limit Cap
You accept any limit. A client requests 10000 items. Fix: cap the limit. Math.min(parseInt(req.query.limit) || 20, 100).
Mistake 3: skip for Large Offsets
You use skip=100000 for page 5001. MongoDB scans the first 100000 docs. Fix: use cursor-based pagination for large collections.
Mistake 4: No Index on the Sort Field
You sort by createdAt but did not index it. MongoDB sorts in memory (slow). Fix: index the sort field. Use a compound index with the filter field.
Mistake 5: Unstable Sort
You sort by createdAt. Two items have the same timestamp. Cursor pagination skips or duplicates items. Fix: add a tiebreaker. .sort({ createdAt: -1, _id: 1 }).
Mistake 6: Running find and count Sequentially
You await find, then await countDocuments. Waterfall delay. Fix: use Promise.all([find, countDocuments]) to run both in parallel.
Mistake 7: Returning null for Empty Results
You return null when there are no items. The client has to handle null. Fix: return an empty array. { data: [], total: 0 }.
Mistake 8: Not Returning Metadata
You return just the items. The client does not know if there is more. Fix: return { data, total, skip, limit } for offset, or { data, nextCursor, hasMore } for cursor.
Mistake 9: Pagination in the Body
You put skip and limit in the request body. GET requests should not have a body. Fix: use query params. ?skip=0&limit=20.
Mistake 10: Not Handling Invalid Cursors
You accept any cursor string. An invalid cursor crashes the server. Fix: validate the cursor. For a date cursor, try new Date(cursor) and reject if invalid.
The Takeaway
Common pagination mistakes: no pagination, no limit cap, skip for large offsets, no index on the sort field, unstable sort, sequential find and count, returning null for empty results, not returning metadata, pagination in the body, and not handling invalid cursors. Fix them on day one.
No pagination, no limit cap, skip for large offsets, no index on the sort field, unstable sort, sequential find and count, returning null for empty results, not returning metadata, pagination in the body, and not handling invalid cursors.
MongoDB still scans the skipped documents before returning the ones you want. For offset 100000, it scans 100000 docs. Use cursor-based pagination instead: find documents where createdAt is less than the last cursor. MongoDB uses an index.
If you sort by createdAt, ties are possible. Without a tiebreaker, cursor pagination can skip or duplicate items. Use .sort({ createdAt: -1, _id: 1 }) to make the sort stable.
To avoid waterfall delay. If you await find, then await countDocuments, the total query waits for the find to finish. Promise.all([find, countDocuments]) runs both at once. Faster.
So the client does not have to handle null. Return { data: [], total: 0 } for offset or { data: [], nextCursor: null, hasMore: false } for cursor. Consistent shapes make the client simpler and less bug-prone.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

