Mongoose Paginate для JavaScript (Node.js): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Mongoose Paginate для JavaScript (Node.js): полное руководство с примерами

Добро пожаловать на PythonLib! Сегодня мы разберем Mongoose Paginate — одну из самых популярных библиотек для реализации пагинации в приложениях на Node.js, использующих Mongoose ODM (Object Data Modeling) для MongoDB.

1. Что это и зачем нужна

Mongoose Paginate — это легковесный плагин для Mongoose, который добавляет метод paginate() к вашим схемам. Он позволяет легко разбивать большие наборы данных на страницы, избегая загрузки всех документов сразу.

Зачем это нужно? В реальных приложениях (блоги, интернет-магазины, API) базы данных содержат тысячи, а иногда и миллионы записей. Загружать их все одновременно — это:

  • Огромная нагрузка на сервер (память, процессор).
  • Медленная работа API (пользователь ждет ответа минутами).
  • Плохой UX (бесконечный скролл без пагинации или кнопок "Далее").

Mongoose Paginate решает эти проблемы, предоставляя готовую, протестированную и гибкую реализацию пагинации. Она автоматически считает общее количество документов, возвращает нужную страницу и предоставляет мета-информацию (текущая страница, всего страниц, есть ли следующая/предыдущая).

2. Установка

Установка производится через npm (Node Package Manager). Убедитесь, что у вас уже установлен Node.js и npm.

npm install mongoose-paginate-v2

Важно: Существует две версии библиотеки:

  • mongoose-paginate — старая, менее поддерживаемая версия.
  • mongoose-paginate-v2 — актуальная, рекомендуемая версия с большим количеством возможностей и исправлений.

В этой статье мы будем использовать mongoose-paginate-v2.

3. Быстрый старт — минимальный рабочий пример

Давайте создадим простую модель "Пользователь" и применим к ней пагинацию.

// 1. Подключаем зависимости
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

// 2. Определяем схему
const userSchema = new mongoose.Schema({
    name: String,
    email: String,
    age: Number,
    createdAt: { type: Date, default: Date.now }
});

// 3. Добавляем плагин пагинации к схеме
userSchema.plugin(mongoosePaginate);

// 4. Создаем модель
const User = mongoose.model('User', userSchema);

// 5. Подключаемся к MongoDB (пример с локальной БД)
async function main() {
    await mongoose.connect('mongodb://localhost:27017/testdb');

    // 6. Вставляем тестовые данные (если их нет)
    const count = await User.countDocuments();
    if (count === 0) {
        const users = [];
        for (let i = 1; i <= 50; i++) {
            users.push({ name: `User ${i}`, email: `user${i}@example.com`, age: 20 + (i % 30) });
        }
        await User.insertMany(users);
        console.log('Вставлено 50 пользователей');
    }

    // 7. Используем пагинацию
    const options = {
        page: 1,      // Номер страницы (по умолчанию 1)
        limit: 10,    // Количество элементов на странице (по умолчанию 10)
    };

    const result = await User.paginate({}, options);

    console.log('Результат пагинации:');
    console.log('Документы:', result.docs);
    console.log('Всего документов:', result.totalDocs);
    console.log('Всего страниц:', result.totalPages);
    console.log('Текущая страница:', result.page);
    console.log('Есть следующая страница:', result.hasNextPage);
    console.log('Есть предыдущая страница:', result.hasPrevPage);

    await mongoose.disconnect();
}

main().catch(console.error);

Этот код создаст 50 пользователей, а затем запросит первую страницу с 10 пользователями. В консоли вы увидите не только сами документы, но и мета-информацию.

4. Основные методы/функции/классы

Библиотека mongoose-paginate-v2 предоставляет один главный метод — paginate(), но его гибкость достигается за счет множества опций. Рассмотрим ключевые из них.

4.1. Model.paginate([query], [options], [callback])

Сигнатура: Model.paginate(query, options, callback)

Что делает: Основной метод для выполнения пагинированного запроса. Принимает фильтр (query), опции пагинации и опциональный callback. Если callback не передан, возвращает Promise.

const result = await User.paginate({ age: { $gte: 25 } }, { page: 2, limit: 5 });

4.2. Опция page

Сигнатура: { page: Number }

Что делает: Определяет номер запрашиваемой страницы. Нумерация начинается с 1. По умолчанию: 1.

const result = await User.paginate({}, { page: 3 }); // Получить 3-ю страницу

4.3. Опция limit

Сигнатура: { limit: Number }

Что делает: Определяет максимальное количество документов на одной странице. По умолчанию: 10.

const result = await User.paginate({}, { limit: 25 }); // 25 пользователей на странице

4.4. Опция sort

Сигнатура: { sort: Object | String }

Что делает: Определяет порядок сортировки документов. Работает так же, как и в обычном find().sort().

const result = await User.paginate({}, { 
    sort: { createdAt: -1 } // Сортировка по дате создания (новые сверху)
});

4.5. Опция select

Сигнатура: { select: Object | String }

Что делает: Позволяет выбрать только определенные поля документа. Экономит трафик и ускоряет запросы.

const result = await User.paginate({}, { 
    select: 'name email' // Вернуть только name и email
});

4.6. Опция populate

Сигнатура: { populate: Object | Array }

Что делает: Позволяет "развернуть" ссылки (refs) на другие коллекции, аналогично методу .populate() в Mongoose. Может быть массивом для множественных populate.

// Предположим, у пользователя есть поле 'posts', ссылающееся на модель Post
const result = await User.paginate({}, { 
    populate: { path: 'posts', select: 'title' }
});

4.7. Опция lean

Сигнатура: { lean: Boolean }

Что делает: Если установлено в true, возвращает простые JavaScript-объекты вместо полноценных документов Mongoose. Это значительно ускоряет запросы, но вы теряете методы Mongoose (например, .save()). Рекомендуется для read-only API.

const result = await User.paginate({}, { lean: true }); // Быстрее, но без методов Mongoose

4.8. Опция customLabels

Сигнатура: { customLabels: Object }

Что делает: Позволяет переименовать поля в возвращаемом результате. Полезно, если ваш фронтенд ожидает другие названия (например, items вместо docs).

const myCustomLabels = {
    totalDocs: 'itemCount',
    docs: 'items',
    page: 'currentPage',
    nextPage: 'next',
    prevPage: 'prev',
    totalPages: 'pageCount',
    hasNextPage: 'hasNext',
    hasPrevPage: 'hasPrev'
};

const result = await User.paginate({}, { customLabels: myCustomLabels });
console.log(result.items); // Вместо result.docs
console.log(result.itemCount); // Вместо result.totalDocs

5. Реальный практический пример (100-200 строк кода)

Давайте создадим полноценный REST API для блога с использованием Express, Mongoose и Mongoose Paginate.

// server.js
const express = require('express');
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

const app = express();
app.use(express.json());

// --- Модель ---
const postSchema = new mongoose.Schema({
    title: { type: String, required: true },
    content: { type: String, required: true },
    author: { type: String, required: true },
    tags: [String],
    published: { type: Boolean, default: false },
    createdAt: { type: Date, default: Date.now }
});

postSchema.plugin(mongoosePaginate);

const Post = mongoose.model('Post', postSchema);

// --- Подключение к БД ---
mongoose.connect('mongodb://localhost:27017/blogdb')
    .then(() => console.log('MongoDB подключена'))
    .catch(err => console.error('Ошибка подключения:', err));

// --- Эндпоинт для создания поста ---
app.post('/api/posts', async (req, res) => {
    try {
        const post = new Post(req.body);
        await post.save();
        res.status(201).json(post);
    } catch (err) {
        res.status(400).json({ error: err.message });
    }
});

// --- Эндпоинт для получения постов с пагинацией ---
app.get('/api/posts', async (req, res) => {
    try {
        // Параметры из query-строки с значениями по умолчанию
        const page = parseInt(req.query.page) || 1;
        const limit = parseInt(req.query.limit) || 10;
        const sort = req.query.sort || '-createdAt'; // По умолчанию новые сверху
        const author = req.query.author;
        const tag = req.query.tag;
        const published = req.query.published;

        // Строим фильтр
        const filter = {};
        if (author) filter.author = author;
        if (tag) filter.tags = { $in: [tag] };
        if (published !== undefined) filter.published = published === 'true';

        // Опции пагинации
        const options = {
            page,
            limit,
            sort,
            lean: true, // Для read-only API это быстрее
            select: 'title author tags published createdAt', // Не возвращаем content для списка
            customLabels: {
                docs: 'posts',
                totalDocs: 'totalPosts',
                page: 'currentPage',
                totalPages: 'totalPages'
            }
        };

        const result = await Post.paginate(filter, options);

        res.json({
            success: true,
            ...result,
            // Добавляем удобные ссылки для API
            links: {
                self: `/api/posts?page=${result.currentPage}&limit=${limit}`,
                next: result.hasNextPage ? `/api/posts?page=${result.nextPage}&limit=${limit}` : null,
                prev: result.hasPrevPage ? `/api/posts?page=${result.prevPage}&limit=${limit}` : null
            }
        });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

// --- Эндпоинт для получения одного поста (без пагинации) ---
app.get('/api/posts/:id', async (req, res) => {
    try {
        const post = await Post.findById(req.params.id);
        if (!post) return res.status(404).json({ error: 'Пост не найден' });
        res.json(post);
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

// --- Запуск сервера ---
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Сервер запущен на порту ${PORT}`);
    // Добавим тестовые данные при первом запуске
    seedData();
});

async function seedData() {
    const count = await Post.countDocuments();
    if (count === 0) {
        const posts = [];
        const authors = ['Alice', 'Bob', 'Charlie'];
        const tags = ['javascript', 'nodejs', 'mongodb', 'tutorial', 'api'];
        
        for (let i = 1; i <= 100; i++) {
            posts.push({
                title: `Post ${i}: How to use ${tags[i % tags.length]}`,
                content: `This is the content of post ${i}. It contains a lot of useful information about ${tags[i % tags.length]}.`,
                author: authors[i % authors.length],
                tags: [tags[i % tags.length], tags[(i + 1) % tags.length]],
                published: i % 3 !== 0 // 2/3 постов опубликованы
            });
        }
        
        await Post.insertMany(posts);
        console.log('Добавлено 100 тестовых постов');
    }
}

Как использовать:

  1. Запустите сервер: node server.js
  2. Создайте пост: POST /api/posts с JSON-телом.
  3. Получите список постов: GET /api/posts?page=2&limit=5&author=Alice
  4. Попробуйте фильтры: GET /api/posts?tag=mongodb&published=true

6. Советы по производительности и типичные ошибки

Советы по производительности:

  1. Используйте lean: true для read-only запросов. Это уменьшает накладные расходы на создание полноценных документов Mongoose.
  2. Ограничивайте

Рекомендаций