Skip to content

Pagination

intermediate2 min read
  • REST API
  • Node.js

Returning a large collection in smaller pages instead of all at once, using offset or cursor strategies to keep responses fast and bounded.

What you'll understand

  • Why endpoints that return collections must paginate
  • The difference between offset and cursor pagination
  • How a paginated response tells a client there is more to fetch

Explanation

Pagination is returning a large collection in smaller, bounded chunks called pages, rather than every row at once. A list endpoint that returns all records works fine with ten rows and falls over with ten million: the query is slow, the response is huge, and the client struggles to handle it. Pagination keeps every response fast and predictable regardless of how much data exists.

There are two common strategies. Offset pagination uses a page number and size (skip this many rows, take that many); it is simple and lets a client jump to any page, but it grows slower deep into a list and can skip or repeat rows if data changes between requests. Cursor pagination returns items after a stable pointer, usually the id of the last item seen; it is efficient and consistent for large or fast-changing data, at the cost of not jumping to arbitrary pages.

Whichever you choose, the response must tell the client how to continue. Offset responses typically include the page, page size, and total count; cursor responses include the cursor for the next page. Without that metadata a client cannot know whether more data remains (this pairs naturally with a consistent collection response format).

Examples

This TypeScript example implements offset pagination with a Prisma query:

const page = Number(req.query.page ?? 1);
const pageSize = 20;

const items = await prisma.user.findMany({
  skip: (page - 1) * pageSize,
  take: pageSize,
  orderBy: { id: "asc" },
});

This example uses cursor pagination, fetching the items after a cursor:

const items = await prisma.user.findMany({
  take: 20,
  skip: 1, // skip the cursor itself
  cursor: { id: lastSeenId },
  orderBy: { id: "asc" },
});

Common mistakes

  • Returning an entire collection with no pagination at all.
  • Allowing an unbounded page size so a client can request everything anyway.
  • Using offset pagination on very large lists and paying for it deep in the data.
  • Omitting the metadata a client needs to request the next page.

Best practices

  • Paginate every endpoint that returns a collection.
  • Enforce a sensible default and maximum page size.
  • Prefer cursor pagination for large or frequently changing datasets.
  • Always paginate over a stable sort order (see Related Topics).

Further reading

Related topics