better-drizzle

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:

InputMeaning
page1-based page number; computes skip and cannot be combined with it
perPage / limitpage size, default 10
takepage size; wins over perPage and limit
skiprows 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, ascending

Cursor 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
});

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

StrategyGood forTrade-off
Offsetadmin tables, reporting, simple listseasy to reason about; weaker on very large, changing datasets
Cursorfeeds, timelines, large mutable datasetsneeds stable ordering and cursor discipline

On this page