Pagination
Paginate query results using offset-based or cursor-based pagination
Offset Pagination
Simple page-based pagination:
// Page 1 (first 10 records)
const page1 = await client.user.findMany({
take: 10,
skip: 0,
});
// Page 2
const page2 = await client.user.findMany({
take: 10,
skip: 10,
});
// Page N
const pageN = await client.user.findMany({
take: 10,
skip: (page - 1) * 10,
});
With Total Count
async function getPage(page: number, pageSize: number) {
const [items, total] = await Promise.all([
client.user.findMany({
take: pageSize,
skip: (page - 1) * pageSize,
orderBy: { createdAt: "desc" },
}),
client.user.count(),
]);
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
Cursor Pagination
More efficient for large datasets:
// First page
const page1 = await client.user.findMany({
take: 10,
orderBy: { id: "asc" },
});
// Next page (using last item's ID as cursor)
const page2 = await client.user.findMany({
take: 10,
skip: 1, // Skip the cursor itself
cursor: { id: lastId },
orderBy: { id: "asc" },
});
Cursor queries are deterministic
- Your
orderBykeys apply in the order you declare them, and VibORM appends the primary key as a final ascending tie-breaker — so every cursor query has one deterministic order, and tied or null values never cause duplicated or skipped records between pages. - A bare ascending order places
nulllast; a bare descending order placesnullfirst. Use{ sort, nulls }to choose explicitly. - A cursor that doesn’t match an existing row returns an empty page.
- A
cursortakes unique discriminators only. Unlike thewhereoffindUnique, it does not accept extra scalar filters — a cursor is an exact row address, not a lookup to narrow. - Cursor pagination works with plain scalar
asc/descordering only — relation ordering and vector-distance ordering must be queried without a cursor.
Bi-directional Cursor
Navigate forward and backward:
// Forward (next)
const next = await client.user.findMany({
take: 10,
skip: 1,
cursor: { id: cursorId },
orderBy: { id: "asc" },
});
// Backward (previous)
const prev = await client.user.findMany({
take: -10, // Negative take = backward
skip: 1,
cursor: { id: cursorId },
orderBy: { id: "asc" },
});
With a negative take, results still come back in the original order — don’t
reverse the returned page yourself.
take and skip
| Option | Description |
|---|---|
take |
Number of records to return (positive = forward, negative = backward) |
skip |
Number of records to skip |
cursor |
Start position for cursor pagination |
Examples
Infinite Scroll
async function loadMore(cursor?: string, limit = 20) {
const items = await client.post.findMany({
take: limit + 1, // Fetch one extra to check if more exist
...(cursor && {
skip: 1,
cursor: { id: cursor },
}),
orderBy: { createdAt: "desc" },
});
const hasMore = items.length > limit;
const data = hasMore ? items.slice(0, -1) : items;
return {
data,
hasMore,
nextCursor: hasMore ? data[data.length - 1].id : undefined,
};
}
Relay-Style Pagination
interface Connection<T> {
edges: { node: T; cursor: string }[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
};
}
async function getConnection(
first?: number,
after?: string,
last?: number,
before?: string
): Promise<Connection<User>> {
const pageSize = first ?? last ?? 10;
const take = first !== undefined ? pageSize + 1 : -(pageSize + 1);
const cursor = after ?? before;
const items = await client.user.findMany({
take,
...(cursor && { skip: 1, cursor: { id: cursor } }),
orderBy: { id: "asc" },
});
const hasMore = items.length > pageSize;
let nodes = items;
if (hasMore) {
nodes = first !== undefined ? items.slice(0, -1) : items.slice(1);
}
return {
edges: nodes.map(node => ({
node,
cursor: node.id,
})),
pageInfo: {
hasNextPage: first ? hasMore : false,
hasPreviousPage: last ? hasMore : false,
startCursor: nodes[0]?.id,
endCursor: nodes[nodes.length - 1]?.id,
},
};
}
Offset vs Cursor
| Aspect | Offset | Cursor |
|---|---|---|
| Simplicity | ✅ Simple | ⚠️ More complex |
| Performance | ⚠️ Degrades on large offsets | ✅ Consistent |
| Random access | ✅ Any page | ❌ Sequential only |
| Real-time data | ⚠️ Can skip/duplicate | ✅ Consistent |
distinct
Return unique values:
// Distinct roles
const roles = await client.user.findMany({
distinct: ["role"],
select: { role: true },
});
// Distinct by multiple fields
const unique = await client.post.findMany({
distinct: ["authorId", "categoryId"],
});