Skip to content

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

OptionDefaultDescription
page1Page number (1-based)
limit20Items 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:

  1. Caching the total count
  2. Using estimatedDocumentCount() for approximate totals
  3. 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,
  };
}

Released under the MIT License.