Facebook Pixel

API Pagination, Filtering, and Sorting in Express

List endpoints need pagination, filtering, and sorting. Here is how to do all three.

API Pagination, Filtering, and Sorting in Express

List endpoints should never return the entire collection. They need pagination, filtering, and sorting. Here is how to do all three in Express.

Pagination

Use skip and limit:

router.get('/users', asyncHandler(async (req, res) => {
  const skip = parseInt(req.query.skip) || 0;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const users = await User.find().skip(skip).limit(limit).lean();
  res.json(users);
}));

Always cap limit (e.g., max 100) to prevent abuse.

Cursor-Based Pagination

For very large collections, skip is slow (MongoDB still scans skipped docs). Use cursor-based pagination:

router.get('/messages', asyncHandler(async (req, res) => {
  const cursor = req.query.cursor ? new Date(req.query.cursor) : new Date();
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const messages = await Message.find({ createdAt: { $lt: cursor } })
    .sort({ createdAt: -1 })
    .limit(limit)
    .lean();
  const nextCursor = messages.length === limit ? messages[messages.length - 1].createdAt : null;
  res.json({ data: messages, nextCursor });
}));

Filtering

Use query params:

router.get('/users', asyncHandler(async (req, res) => {
  const filter = {};
  if (req.query.role) filter.role = req.query.role;
  if (req.query.gender) filter.gender = req.query.gender;
  if (req.query.minAge) filter.age = { $gte: parseInt(req.query.minAge) };
  const users = await User.find(filter).lean();
  res.json(users);
}));

Sorting

router.get('/users', asyncHandler(async (req, res) => {
  const sortBy = req.query.sortBy || 'createdAt';
  const order = req.query.order === 'asc' ? 1 : -1;
  const users = await User.find().sort({ [sortBy]: order }).lean();
  res.json(users);
}));

Whitelist sortable fields to prevent arbitrary sorting.

Field Selection

Let clients pick fields:

router.get('/users', asyncHandler(async (req, res) => {
  const fields = req.query.fields ? req.query.fields.split(',').join(' ') : null;
  const users = await User.find().select(fields).lean();
  res.json(users);
}));

Combined: Pagination, Filter, Sort, Select

router.get('/users', asyncHandler(async (req, res) => {
  const skip = parseInt(req.query.skip) || 0;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const filter = buildFilter(req.query);
  const sort = buildSort(req.query);
  const select = req.query.fields ? req.query.fields.split(',').join(' ') : null;
  const [users, total] = await Promise.all([
    User.find(filter).sort(sort).skip(skip).limit(limit).select(select).lean(),
    User.countDocuments(filter)
  ]);
  res.json({ data: users, total, skip, limit });
}));

Returning total helps clients show "showing 1-20 of 432".

The Takeaway

For list endpoints, support pagination (skip and limit, or cursor), filtering (build a filter from query params), sorting (whitelist sortable fields), and field selection. Cap limit. Return total for offset pagination. Use cursor-based pagination for very large collections.

Use skip and limit: User.find().skip(parseInt(req.query.skip) || 0).limit(Math.min(parseInt(req.query.limit) || 20, 100)). Always cap limit (e.g., max 100) to prevent abuse.

Instead of skip (which scans skipped docs), use a cursor like the last createdAt. Find documents where createdAt is less than the cursor. Faster for very large collections because MongoDB uses an index.

Build a filter object from query params. if (req.query.role) filter.role = req.query.role. Pass the filter to User.find(filter). Whitelist allowed filters to prevent arbitrary queries.

Use sortBy and order query params. const sortBy = req.query.sortBy || 'createdAt'; const order = req.query.order === 'asc' ? 1 : -1. User.find().sort({ [sortBy]: order }). Whitelist sortable fields.

Clients can show 'showing 1-20 of 432' and decide whether to show a next button. Use Promise.all([find, countDocuments]) to run both queries in parallel and avoid waterfall delays.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.