Script Valley
REST API Development: Beginner to Production
Data Validation, Error Handling, and API Design PatternsLesson 3.3

API pagination — cursor-based vs offset-based

offset pagination, cursor pagination, page and limit params, total count, next/prev links, cursor encoding, performance tradeoffs

API Pagination Strategies

Never return unbounded lists from an API. Pagination is required for any endpoint that returns a collection. Two main approaches: offset and cursor.

Offset Pagination

Simple to implement. Takes page and limit query params. The problem: skipping rows gets slower as offset grows, and concurrent inserts cause rows to appear or disappear between pages.

// GET /posts?page=2&limit=20
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;

const posts = await Post.findAll({ limit, offset });
const total = await Post.count();

res.json({
  data: posts,
  pagination: {
    page, limit, total,
    totalPages: Math.ceil(total / limit),
    hasNext: page * limit < total
  }
});

Cursor Pagination

Uses an opaque cursor (usually a base64-encoded ID or timestamp) to mark the last seen record. Stable under concurrent writes and consistent performance regardless of dataset size. Preferred for real-time feeds and large tables.

// GET /posts?cursor=eyJpZCI6NTB9&limit=20
const cursor = req.query.cursor
  ? JSON.parse(Buffer.from(req.query.cursor, 'base64').toString())
  : null;

const where = cursor ? { id: { $gt: cursor.id } } : {};
const posts = await Post.findAll({ where, limit, orderBy: 'id' });
const nextCursor = posts.length === limit
  ? Buffer.from(JSON.stringify({ id: posts.at(-1).id })).toString('base64')
  : null;

res.json({ data: posts, nextCursor });

Up next

API filtering and sorting with query parameters

Sign in to track progress