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 with Middleware

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

const app = express()

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

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

    res.json(res.paginate(posts, total))
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

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 }

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

    query.order = [['id', 'DESC']]

    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 })
  }
})

Sequelize Pagination

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 })
  }
})

Prisma Pagination

const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()

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

  try {
    const [posts, total] = await Promise.all([
      prisma.post.findMany({
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { createdAt: 'desc' }
      }),
      prisma.post.count()
    ])

    res.json({
      data: posts,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / 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 })
  }
})
function buildLinkHeader(baseUrl, page, limit, total) {
  const totalPages = Math.ceil(total / limit)
  const links = []

  if (page > 1) {
    links.push(`<${baseUrl}?page=1&limit=${limit}>; rel="first"`)
    links.push(`<${baseUrl}?page=${page - 1}&limit=${limit}>; rel="prev"`)
  }

  if (page < totalPages) {
    links.push(`<${baseUrl}?page=${page + 1}&limit=${limit}>; rel="next"`)
    links.push(`<${baseUrl}?page=${totalPages}&limit=${limit}>; rel="last"`)
  }

  return links.join(', ')
}

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

  const users = await db.users.findAll({
    limit,
    offset: (page - 1) * limit
  })

  const total = await db.users.count()

  res.set('Link', buildLinkHeader('/api/users', page, limit, total))
  res.set('X-Total-Count', total)

  res.json(users)
})

Keyset Pagination (Performance Optimized)

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

  try {
    let where = {}

    if (lastId && lastCreatedAt) {
      where = {
        [Op.or]: [
          { createdAt: { [Op.lt]: lastCreatedAt } },
          {
            [Op.and]: [
              { createdAt: lastCreatedAt },
              { id: { [Op.lt]: lastId } }
            ]
          }
        ]
      }
    }

    const articles = await Article.findAll({
      where,
      limit: limit + 1,
      order: [
        ['createdAt', 'DESC'],
        ['id', 'DESC']
      ]
    })

    const hasNext = articles.length > limit
    if (hasNext) {
      articles.pop()
    }

    const lastArticle = articles[articles.length - 1]

    res.json({
      data: articles,
      pagination: {
        hasNext,
        limit,
        nextCursor: hasNext
          ? {
              lastId: lastArticle.id,
              lastCreatedAt: lastArticle.createdAt
            }
          : null
      }
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Pagination Helper Class

class Paginator {
  constructor(model, query = {}) {
    this.model = model
    this.query = query
  }

  async paginate(page = 1, limit = 10) {
    const offset = (page - 1) * limit

    const [data, total] = await Promise.all([
      this.model.findAll({
        ...this.query,
        limit,
        offset
      }),
      this.model.count({ where: this.query.where })
    ])

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

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

  const paginator = new Paginator(User, {
    where: { active: true },
    order: [['createdAt', 'DESC']]
  })

  const result = await paginator.paginate(page, limit)
  res.json(result)
})

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 implement filtering in Node.js and how to implement sorting in Node.js.


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.
How to convert a string to boolean in JavaScript
How to convert a string to boolean in JavaScript

How to concatenate a strings in JavaScript?
How to concatenate a strings in JavaScript?

JavaScript printf equivalent
JavaScript printf equivalent

How to sort an array of objects by string property value in JavaScript
How to sort an array of objects by string property value in JavaScript

Answers by CoreUI Core Team