Pagination
Monch provides built-in pagination with metadata for building paginated UIs.
Basic Usage
typescript
const result = await Users.find({ status: 'active' })
.sort({ createdAt: -1 })
.paginate({ page: 1, limit: 20 });Result Structure
typescript
{
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) |
Limit Cap
The limit is silently capped at 100 to prevent accidentally loading too much data. If you need more than 100 items, use multiple requests or streaming.
Examples
Simple Pagination
typescript
const page1 = await Users.find().paginate({ page: 1 });
const page2 = await Users.find().paginate({ page: 2 });With Filters and Sorting
typescript
const result = await Posts.find({
status: 'published',
category: 'tech',
})
.sort({ publishedAt: -1 })
.paginate({ page: 3, limit: 10 });Cursor Methods Still Work
typescript
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
typescript
// 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
typescript
// 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
typescript
// 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:
typescript
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:
typescript
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:
typescript
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,
};
}