DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

API Pagination: The Right Way to Handle Large Datasets

API Pagination: The Right Way to Handle Large Datasets

As of February 2026, poorly paginated APIs remain one of the top causes of client frustration and performance issues. Here is how to do it right.

The Problem

When your API returns thousands of records, sending everything at once is a disaster waiting to happen. Clients timeout, servers choke, and everyone has a bad time.

5 Pagination Best Practices

1. Use Cursor-Based Pagination

Offset pagination seems easier but falls apart at scale.

// Bad: Offset pagination breaks with large datasets
GET /users?page=2&limit=50

// Good: Cursor-based is reliable
GET /users?cursor=eyJpZCI6MTAwfQ&limit=50
Enter fullscreen mode Exit fullscreen mode

Cursor pagination uses an opaque token (usually the last ID or a timestamp) instead of page numbers. It is faster and will not skip items when new data is inserted.

2. Always Return Metadata

Clients need to know there is more data:

{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTAwfQ",
    "has_more": true,
    "total": 1247
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Set Reasonable Limits

Do not let clients request 100,000 records. Cap it:

const MAX_LIMIT = 100;
const DEFAULT_LIMIT = 20;

const limit = Math.min(req.query.limit || DEFAULT_LIMIT, MAX_LIMIT);
Enter fullscreen mode Exit fullscreen mode

4. Support Both Directions

Power users need to navigate backward too:

GET /users?cursor=abc&limit=20&direction=forward
GET /users?cursor=xyz&limit=20&direction=backward
Enter fullscreen mode Exit fullscreen mode

5. Be Consistent Across Endpoints

Do not use page numbers on one endpoint and cursors on another. Pick one strategy and use it everywhere.

Quick Implementation in Node.js

app.get(/api/users, async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  const query = cursor 
    ? { _id: { $lt: cursor } }
    : {};

  const users = await User.find(query)
    .sort({ _id: -1 })
    .limit(parseInt(limit) + 1);

  const hasMore = users.length > parseInt(limit);
  const data = hasMore ? users.slice(0, -1) : users;
  const nextCursor = hasMore ? data[data.length - 1]._id : null;

  res.json({ data, pagination: { next_cursor: nextCursor, has_more: hasMore } });
});
Enter fullscreen mode Exit fullscreen mode

The Bottom Line

Cursor-based pagination is the winner for production APIs. It scales, stays fast, and handles real-world data changes gracefully.


What is your pagination strategy? Drop a comment below.

Top comments (0)