Pagination
Monch provides built-in pagination with metadata for building paginated UIs.
Basic Usage
const result = await Users.find({ status: 'active' })
.sort({ createdAt: -1 })
.paginate({ page: 1, limit: 20 });Result Structure
{
data: User[], // Array of documents for this page
pagination: {
page: number, // Current page (1-based)
limit: number, // Items per page
total: number, // Total matching documents
totalPages: number, // Total number of pages
hasNext: boolean, // Has more pages after this
hasPrev: boolean, // Has pages before this
}
}Options
| Option | Default | Description |
|---|---|---|
page | 1 | Page number (1-based) |
limit | 20 | Items per page (capped at 100) |
Silent Limit Cap
Requests for limit values greater than 100 are silently capped to 100. The pagination result will show the capped limit, not your requested value:
const result = await Users.find().paginate({ page: 1, limit: 500 });
// Check what limit was actually applied
console.log(result.pagination.limit); // 100 (not 500!)If you need more than 100 items, use multiple paginated requests or cursor-based iteration.
Examples
Simple Pagination
const page1 = await Users.find().paginate({ page: 1 });
const page2 = await Users.find().paginate({ page: 2 });With Filters and Sorting
const result = await Posts.find({
status: 'published',
category: 'tech',
})
.sort({ publishedAt: -1 })
.paginate({ page: 3, limit: 10 });Cursor Methods Still Work
const result = await Users.find({ role: 'admin' })
.sort({ name: 1 })
.project({ name: 1, email: 1 }) // Select fields
.paginate({ page: 1, limit: 50 });Skip and Limit
Don't use .skip() or .limit() with .paginate(). The pagination method handles these internally. If you do use them, they will be ignored.
Next.js Integration
Server Component
// app/users/page.tsx
import { Users } from '@/lib/models';
import { UserList } from './UserList';
import { Pagination } from '@/components/Pagination';
interface Props {
searchParams: { page?: string };
}
export default async function UsersPage({ searchParams }: Props) {
const page = Number(searchParams.page) || 1;
const result = await Users.find({ status: 'active' })
.sort({ createdAt: -1 })
.paginate({ page, limit: 20 });
const users = result.data.map(u => u.serialize());
return (
<>
<UserList users={users} />
<Pagination {...result.pagination} />
</>
);
}Server Action
// app/actions/user.actions.ts
'use server';
import { Users } from '@/lib/models';
export async function getUsers(page: number, filters?: { role?: string }) {
const query = filters?.role ? { role: filters.role } : {};
const result = await Users.find(query)
.sort({ createdAt: -1 })
.paginate({ page, limit: 20 });
return {
users: result.data.map(u => u.serialize()),
pagination: result.pagination,
};
}Pagination Component
// components/Pagination.tsx
'use client';
import Link from 'next/link';
interface Props {
page: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
export function Pagination({ page, totalPages, hasNext, hasPrev }: Props) {
return (
<nav className="flex gap-2">
{hasPrev && (
<Link href={`?page=${page - 1}`}>Previous</Link>
)}
<span>Page {page} of {totalPages}</span>
{hasNext && (
<Link href={`?page=${page + 1}`}>Next</Link>
)}
</nav>
);
}Performance Considerations
Counting
Pagination requires counting total documents, which can be slow on large collections. For frequently accessed pages, consider:
- Caching the total count
- Using
estimatedDocumentCount()for approximate totals - Infinite scroll instead of pagination (no total needed)
Index Usage
Ensure your filter and sort fields are indexed:
const Posts = collection({
name: 'posts',
schema: { /* ... */ },
indexes: [
// Index for common queries
{ key: { status: 1, publishedAt: -1 } },
{ key: { category: 1, publishedAt: -1 } },
],
});Alternative: Manual Pagination
If you need more control, you can implement pagination manually:
async function getUsers(page: number, limit: number = 20) {
const skip = (page - 1) * limit;
const [data, total] = await Promise.all([
Users.find({ status: 'active' })
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.toArray(),
Users.count({ status: 'active' }),
]);
return {
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: skip + data.length < total,
hasPrev: page > 1,
},
};
}Cursor-Based Pagination
For very large datasets or real-time data, consider cursor-based pagination:
async function getUsersAfter(lastId: string | null, limit: number = 20) {
const query = lastId
? { _id: { $gt: new ObjectId(lastId) } }
: {};
const users = await Users.find(query)
.sort({ _id: 1 })
.limit(limit + 1) // Fetch one extra to check for next page
.toArray();
const hasNext = users.length > limit;
if (hasNext) users.pop(); // Remove the extra item
return {
data: users,
nextCursor: hasNext ? users[users.length - 1]._id.toString() : null,
};
}