How to Implement Infinite Scroll in Node.js
Infinite scroll needs cursor pagination. Here is how to implement it in Node.js.
How to Implement Infinite Scroll in Node.js
Infinite scroll needs cursor pagination. Here is how to implement it in Node.js.
The Backend
router.get('/feed', asyncHandler(async (req, res) => {
const cursor = req.query.cursor ? new Date(req.query.cursor) : new Date();
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const items = await User.find({
_id: { $ne: req.user.userId },
createdAt: { $lt: cursor }
})
.sort({ createdAt: -1 })
.limit(limit + 1)
.select('firstName photoUrl about')
.lean();
const hasMore = items.length > limit;
if (hasMore) items.pop();
const nextCursor = hasMore ? items[items.length - 1].createdAt : null;
res.json({ data: items, nextCursor, hasMore });
}));
Why limit + 1
Fetch one extra item. If you get limit + 1 items, there is more. If you get limit or fewer, there is no more. This avoids a separate count query.
The Index
userSchema.index({ createdAt: -1 });
Without an index on createdAt, the cursor query is not faster than offset. Index the cursor field.
The Frontend
let cursor = null;
let loading = false;
let hasMore = true;
async function loadMore() {
if (loading || !hasMore) return;
loading = true;
const url = `/api/v1/feed?limit=20${cursor ? '&cursor=' + cursor.toISOString() : ''}`;
const res = await fetch(url);
const { data, nextCursor, hasMore: more } = await res.json();
items.push(...data);
cursor = nextCursor;
hasMore = more;
loading = false;
}
// On scroll near the bottom, call loadMore().
Why Cursor Beats Offset for Infinite Scroll
- Fast: MongoDB uses the index. No scanning of skipped docs.
- Stable: New items inserted between requests do not cause duplicates or skips.
- No count query: limit + 1 tells you if there is more.
The Takeaway
Implement infinite scroll with cursor pagination: fetch limit + 1 items, pop the extra if there is more, return nextCursor and hasMore. Index the cursor field. The frontend keeps calling loadMore() with the last cursor until hasMore is false. Fast, stable, no count query.
Use cursor pagination. Fetch limit + 1 items. If you get limit + 1, there is more; pop the extra. Return nextCursor and hasMore. The frontend keeps calling with the last cursor until hasMore is false. Index the cursor field.
To know if there is more without a separate count query. If you get limit + 1 items, there is more (pop the extra). If you get limit or fewer, there is no more. Avoids the countDocuments query.
Cursor is fast (MongoDB uses the index, no scanning of skipped docs) and stable (new items inserted between requests do not cause duplicates or skips). Offset is slow for large pages and unstable when items are inserted.
An index on the cursor field. For a feed sorted by createdAt descending, use schema.index({ createdAt: -1 }). Without an index, the cursor query is not faster than offset.
Keep a cursor and hasMore flag. On scroll near the bottom, fetch /feed?limit=20&cursor=<lastCursor>. Append the new items. Update cursor and hasMore from the response. Stop when hasMore is false. Use a loading flag to prevent duplicate fetches.
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.

