Pagination
Use paginate() for offset pages and cursor() for cursor navigation.
paginate() is the offset helper. cursor() is the cursor helper. Both return
{ data, pagination }, but the metadata is specific to the strategy.
Offset pagination
Use limit (or take) with skip when the consumer needs totals and page
numbers:
const {
data, // User[]
pagination: { type, page, perPage, total, pageCount, hasNext, hasPrevious }, // type: "offset"
} = await client.users.paginate({
limit: 20,
skip: 40,
orderBy: [{ id: 'asc' }],
where: { active: true },
});paginate() reads these paging inputs:
| Input | Meaning |
|---|---|
page | 1-based page number; computes skip and cannot be combined with it |
perPage / limit | page size, default 10 |
take | page size; wins over perPage and limit |
skip | rows to skip, default 0 |
const { data, pagination } = await client.users.paginate({ page: 3, perPage: 20 });In the result, perPage is the resolved page size, page is
Math.floor(skip / perPage) + 1, and pageCount is 0 when total is 0.
Cursor reads without metadata
findMany({ cursor, take }) returns rows without pagination metadata. When
orderBy is omitted or empty, the first cursor field supplies ascending order.
A negative take selects the closest rows before the cursor and returns them
in that order. Include a unique tie-breaker in orderBy and the cursor when
the main sort field is not unique.
const previous = await client.posts.findMany({
cursor: { id: 50 },
take: -10,
}); // ids 40 through 49, ascendingCursor pagination
Use cursor() for feed-style navigation:
const {
data: first,
pagination: { nextCursor },
} = await client.users.cursor({
limit: 2,
orderBy: [{ id: 'asc' }],
});
const { data: second } = await client.users.cursor({
limit: 2,
orderBy: [{ id: 'asc' }],
after: nextCursor, // { id: 2 }, or null when hasNext is false
});Navigating backwards
Pass before to move backwards. cursor() accepts before or after, never
both.
const {
data: previous,
pagination: { hasPrevious },
} = await client.users.cursor({
limit: 2,
orderBy: [{ id: 'asc' }],
before: { id: 4 },
});cursor() page size comes from limit, then take, and defaults to 10.
after and before take cursor objects, such as the nextCursor /
previousCursor from a previous page. Without orderBy, pages are ordered
by the full primary key ascending. With orderBy, missing primary-key columns
are appended as ascending tiebreakers so rows with the same sort value are not
skipped.
Cursor tokens include every ordered column, including the primary-key
tiebreakers. Pass the returned token unchanged and repeat the same orderBy
for each page. Older cursor tokens without tiebreakers are still accepted;
the next token contains the complete order.
Relation ordering works with offset pages.
Cursor tokens hold only the row's own column values, so cursor() throws
INVALID_ARGS when orderBy contains a
relation key. Use paginate() for
pages sorted by a relation.
Pagination with projection
Both helpers accept the same select / include as a normal read:
const {
data: page,
pagination: { total },
} = await client.posts.paginate({
limit: 10,
orderBy: [{ id: 'desc' }],
include: { author: true },
});
const {
data: feed,
pagination: { hasNext },
} = await client.posts.cursor({
limit: 10,
orderBy: [{ id: 'desc' }],
include: { author: true },
});Choosing offset vs cursor
| Strategy | Good for | Trade-off |
|---|---|---|
| Offset | admin tables, reporting, simple lists | easy to reason about; weaker on very large, changing datasets |
| Cursor | feeds, timelines, large mutable datasets | needs stable ordering and cursor discipline |