Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to implement pagination in Node.js

Pagination divides large datasets into manageable pages, improving API performance and user experience. As the creator of CoreUI with 12 years of Node.js backend experience, I’ve implemented pagination strategies that handle billions of records efficiently while maintaining sub-100ms response times for enterprise applications.

The most scalable approach uses offset-based pagination for simple use cases and cursor-based pagination for real-time data streams.

Offset-Based Pagination

const express = require('express')
const app = express()

app.get('/api/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 10
  const offset = (page - 1) * limit

  try {
    const users = await db.users.findAll({
      limit,
      offset,
      order: [['createdAt', 'DESC']]
    })

    const total = await db.users.count()

    res.json({
      data: users,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
        hasNext: page < Math.ceil(total / limit),
        hasPrev: page > 1
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Pagination Middleware

Create src/middleware/paginate.js:

function paginate(defaultLimit = 10, maxLimit = 100) {
  return (req, res, next) => {
    let page = parseInt(req.query.page) || 1
    let limit = parseInt(req.query.limit) || defaultLimit

    if (page < 1) page = 1
    if (limit < 1) limit = defaultLimit
    if (limit > maxLimit) limit = maxLimit

    req.pagination = {
      page,
      limit,
      offset: (page - 1) * limit
    }

    res.paginate = (data, total) => {
      return {
        data,
        pagination: {
          page,
          limit,
          total,
          totalPages: Math.ceil(total / limit),
          hasNext: page < Math.ceil(total / limit),
          hasPrev: page > 1
        }
      }
    }

    next()
  }
}

module.exports = paginate

Usage:

const paginate = require('./middleware/paginate')

app.get('/api/posts', paginate(20, 100), async (req, res) => {
  const { limit, offset } = req.pagination

  const posts = await db.posts.findAll({ limit, offset })
  const total = await db.posts.count()

  res.json(res.paginate(posts, total))
})

Cursor-Based Pagination

app.get('/api/messages', async (req, res) => {
  const limit = parseInt(req.query.limit) || 20
  const cursor = req.query.cursor

  try {
    const query = {
      limit: limit + 1,
      order: [['id', 'DESC']]
    }

    if (cursor) {
      query.where = { id: { $lt: cursor } }
    }

    const messages = await db.messages.findAll(query)

    const hasNext = messages.length > limit
    if (hasNext) messages.pop()

    const nextCursor = hasNext ? messages[messages.length - 1].id : null

    res.json({
      data: messages,
      pagination: {
        nextCursor,
        hasNext,
        limit
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

MongoDB Pagination

const MongoClient = require('mongodb').MongoClient

app.get('/api/products', async (req, res) => {
  const page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 10
  const skip = (page - 1) * limit

  try {
    const db = req.app.locals.db
    const collection = db.collection('products')

    const [products, total] = await Promise.all([
      collection.find({}).skip(skip).limit(limit).toArray(),
      collection.countDocuments()
    ])

    res.json({
      data: products,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit)
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Pagination with Sequelize

const { Op } = require('sequelize')

app.get('/api/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 10

  try {
    const { count, rows } = await User.findAndCountAll({
      limit,
      offset: (page - 1) * limit,
      order: [['createdAt', 'DESC']]
    })

    res.json({
      data: rows,
      pagination: {
        page,
        limit,
        total: count,
        totalPages: Math.ceil(count / limit)
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Pagination with Filters

app.get('/api/products', async (req, res) => {
  const page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 10
  const { category, minPrice, maxPrice } = req.query

  const where = {}

  if (category) {
    where.category = category
  }

  if (minPrice || maxPrice) {
    where.price = {}
    if (minPrice) where.price.$gte = parseFloat(minPrice)
    if (maxPrice) where.price.$lte = parseFloat(maxPrice)
  }

  try {
    const [products, total] = await Promise.all([
      Product.findAll({
        where,
        limit,
        offset: (page - 1) * limit
      }),
      Product.count({ where })
    ])

    res.json({
      data: products,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit)
      },
      filters: { category, minPrice, maxPrice }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Best Practice Note

This is the same pagination architecture we use in CoreUI enterprise applications. Offset-based pagination works well for small to medium datasets, while cursor-based pagination scales better for large datasets and real-time feeds. Always validate and sanitize page and limit parameters, implement maximum limit caps, and add indexes on columns used for pagination and sorting.

For related API patterns, check out how to handle filtering in Node.js APIs and how to handle sorting in Node.js APIs.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team