SyntaxStudy
Sign Up
Express.js Pagination, Filtering, and Sorting
Express.js Beginner 1 min read

Pagination, Filtering, and Sorting

Production APIs must support pagination to avoid returning unbounded result sets. Cursor-based pagination is more efficient for large datasets than offset/limit because it does not require the database to scan and discard rows. However, offset pagination is simpler and sufficient for most applications. Filtering is expressed through query parameters: `/products?category=electronics&minPrice=100`. Sorting via `/products?sort=price&order=asc`. Your API should document which fields are filterable and sortable, and validate these parameters server-side to prevent injecting arbitrary database column names. A consistent pagination envelope should include the current page data plus metadata: total count, current page, page size, and links to next/previous pages. This envelope is sometimes called a HAL-style response. Always cap the maximum page size (e.g., 100) to prevent clients from fetching the entire dataset in one request.
Example
const express = require('express');
const { query, validationResult } = require('express-validator');
const router  = express.Router();

// Dummy data
const products = Array.from({ length: 200 }, (_, i) => ({
    id:       i + 1,
    name:     `Product ${i + 1}`,
    category: i % 2 === 0 ? 'electronics' : 'clothing',
    price:    Math.round(Math.random() * 500 + 10),
}));

const VALID_SORT_FIELDS = new Set(['id', 'name', 'price']);

router.get('/products',
    query('page').optional().isInt({ min: 1 }).toInt().default(1),
    query('limit').optional().isInt({ min: 1, max: 100 }).toInt().default(20),
    query('sort').optional().isIn([...VALID_SORT_FIELDS]),
    query('order').optional().isIn(['asc', 'desc']),
    query('category').optional().trim(),
    (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });

        let result = [...products];
        const { page, limit, sort, order = 'asc', category } = req.query;

        if (category) result = result.filter(p => p.category === category);
        if (sort)     result.sort((a, b) =>
            order === 'asc' ? a[sort] > b[sort] ? 1 : -1
                             : a[sort] < b[sort] ? 1 : -1);

        const total   = result.length;
        const offset  = (page - 1) * limit;
        const data    = result.slice(offset, offset + limit);

        res.json({ data, meta: { total, page, limit, pages: Math.ceil(total / limit) } });
    }
);

module.exports = router;