Pagination in Sequelize: A Complete Guide

Understanding Pagination

Think of pagination like a book's chapters or a photo album. Instead of showing all content at once, we break it into manageable pieces. Just as a book doesn't show all its pages simultaneously, we can present data in smaller, digestible chunks.

Real-World Examples

  • Social media feeds loading more posts as you scroll
  • E-commerce product listings showing items page by page
  • Search engine results displaying 10 results per page
  • News website articles loading in batches

Basic Pagination Implementation

Let's start with a simple e-commerce product listing:


// models/Product.js
const Product = sequelize.define('Product', {
    name: DataTypes.STRING,
    price: DataTypes.DECIMAL,
    description: DataTypes.TEXT
});

// Product listing with pagination
const getProducts = async (page = 1, limit = 10) => {
    const offset = (page - 1) * limit;
    
    const products = await Product.findAndCountAll({
        limit,
        offset,
        order: [['name', 'ASC']]
    });
    
    return {
        products: products.rows,
        totalItems: products.count,
        totalPages: Math.ceil(products.count / limit),
        currentPage: page
    };
};

// Express route implementation
app.get('/api/products', async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    
    try {
        const result = await getProducts(page, limit);
        res.json(result);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});
                    

Advanced Pagination with Filtering and Sorting

Let's build a more complex product search system:


const searchProducts = async ({
    page = 1,
    limit = 10,
    search = '',
    category = null,
    minPrice = null,
    maxPrice = null,
    sortBy = 'name',
    sortOrder = 'ASC'
}) => {
    const offset = (page - 1) * limit;
    
    // Build where clause
    const whereClause = {};
    if (search) {
        whereClause.name = {
            [Op.iLike]: `%${search}%`
        };
    }
    if (category) {
        whereClause.categoryId = category;
    }
    if (minPrice || maxPrice) {
        whereClause.price = {};
        if (minPrice) whereClause.price[Op.gte] = minPrice;
        if (maxPrice) whereClause.price[Op.lte] = maxPrice;
    }

    const { rows, count } = await Product.findAndCountAll({
        where: whereClause,
        limit,
        offset,
        order: [[sortBy, sortOrder]],
        include: [{
            model: Category,
            attributes: ['name']
        }]
    });

    return {
        products: rows,
        totalItems: count,
        totalPages: Math.ceil(count / limit),
        currentPage: page
    };
};
                    

Building a Social Media Feed

Let's create a complete social media feed system with pagination:


// models/Post.js
const Post = sequelize.define('Post', {
    content: DataTypes.TEXT,
    likes: DataTypes.INTEGER
});

// models/Comment.js
const Comment = sequelize.define('Comment', {
    content: DataTypes.TEXT
});

// Set up associations
Post.hasMany(Comment);
Comment.belongsTo(Post);

// Feed service
const FeedService = {
    async getFeed(userId, page = 1, limit = 10) {
        const offset = (page - 1) * limit;
        
        const { rows, count } = await Post.findAndCountAll({
            include: [
                {
                    model: User,
                    attributes: ['username', 'avatar']
                },
                {
                    model: Comment,
                    limit: 2,  // Show only 2 recent comments per post
                    order: [['createdAt', 'DESC']],
                    include: [{
                        model: User,
                        attributes: ['username']
                    }]
                }
            ],
            order: [['createdAt', 'DESC']],
            limit,
            offset,
            distinct: true  // Important for correct count with associations
        });

        // Format response
        const posts = rows.map(post => ({
            id: post.id,
            content: post.content,
            likes: post.likes,
            author: post.User.username,
            authorAvatar: post.User.avatar,
            commentCount: post.Comments.length,
            recentComments: post.Comments.map(comment => ({
                content: comment.content,
                author: comment.User.username
            }))
        }));

        return {
            posts,
            totalPosts: count,
            totalPages: Math.ceil(count / limit),
            currentPage: page,
            hasMore: page * limit < count
        };
    },

    // Load more comments for a specific post
    async getPostComments(postId, page = 1, limit = 10) {
        const offset = (page - 1) * limit;

        const { rows, count } = await Comment.findAndCountAll({
            where: { postId },
            include: [{
                model: User,
                attributes: ['username', 'avatar']
            }],
            order: [['createdAt', 'DESC']],
            limit,
            offset
        });

        return {
            comments: rows,
            totalComments: count,
            totalPages: Math.ceil(count / limit),
            currentPage: page
        };
    }
};
                    

Best Practices and Tips

  • Always validate and sanitize page and limit parameters
  • Use reasonable default and maximum values for limit
  • Include total count and pagination metadata in responses
  • Consider using cursor-based pagination for real-time data
  • Implement proper error handling for invalid page requests

Common Pitfalls to Avoid

  • Not handling the last page properly
  • Forgetting to count total items
  • Ignoring performance with large offsets
  • Not considering associations in count calculations

Advanced Pagination Techniques

  • Cursor-based pagination
  • Infinite scroll implementation
  • Virtual scrolling for large datasets
  • Caching strategies for paginated data