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.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

