Node.js

Search & Filtering

Patterns for implementing search and filtering in Node.js APIs — from basic SQL LIKE queries to Elasticsearch, query param filtering, sorting, and pagination.

Basic SQL LIKE Search (with mysql2)

Use parameterized queries with LIKE to search across columns. Always sanitize input — never interpolate directly into SQL.

// routes/search.js
const express = require('express');
const router  = express.Router();
const db      = require('../db'); // mysql2 pool

router.get('/products', async (req, res) => {
    const { q = '' } = req.query;
    const term = `%${q}%`;

    const [rows] = await db.query(
        `SELECT id, name, description, price
         FROM products
         WHERE name LIKE ? OR description LIKE ?
         ORDER BY name ASC`,
        [term, term]
    );

    res.json({ data: rows, total: rows.length });
});

module.exports = router;

Filtering, Sorting & Pagination via Query Params

Build dynamic WHERE clauses from query params. Accept page, limit, sort, and order for full control.

// GET /api/products?category=electronics&minPrice=100&sort=price&order=asc&page=2&limit=10

router.get('/api/products', async (req, res) => {
    const {
        category, minPrice, maxPrice,
        sort    = 'created_at',
        order   = 'desc',
        page    = 1,
        limit   = 10,
    } = req.query;

    const allowedSorts  = ['price', 'name', 'created_at'];
    const allowedOrders = ['asc', 'desc'];
    const safeSort  = allowedSorts.includes(sort)   ? sort  : 'created_at';
    const safeOrder = allowedOrders.includes(order) ? order : 'desc';

    const conditions = [];
    const params     = [];

    if (category) {
        conditions.push('category = ?');
        params.push(category);
    }
    if (minPrice) {
        conditions.push('price >= ?');
        params.push(Number(minPrice));
    }
    if (maxPrice) {
        conditions.push('price <= ?');
        params.push(Number(maxPrice));
    }

    const where  = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
    const offset = (Number(page) - 1) * Number(limit);

    const [rows] = await db.query(
        `SELECT * FROM products ${where}
         ORDER BY ${safeSort} ${safeOrder}
         LIMIT ? OFFSET ?`,
        [...params, Number(limit), offset]
    );

    const [[{ total }]] = await db.query(
        `SELECT COUNT(*) AS total FROM products ${where}`,
        params
    );

    res.json({
        data: rows,
        meta: { page: Number(page), limit: Number(limit), total },
    });
});

Elasticsearch Basic Search (official Node.js client)

Use @elastic/elasticsearch for full-text search. The multi_match query searches across multiple fields simultaneously.

npm install @elastic/elasticsearch

// lib/elastic.js
const { Client } = require('@elastic/elasticsearch');

const client = new Client({ node: process.env.ELASTIC_URL || 'http://localhost:9200' });

/**
 * Search products index
 * @param {string} query     - search term
 * @param {object} filters   - { category, minPrice, maxPrice }
 * @param {number} from      - offset (pagination)
 * @param {number} size      - page size
 */
async function searchProducts(query, filters = {}, from = 0, size = 10) {
    const must  = [];
    const filter = [];

    if (query) {
        must.push({
            multi_match: {
                query,
                fields: ['name^3', 'description', 'tags'],
                fuzziness: 'AUTO',
            },
        });
    }

    if (filters.category) {
        filter.push({ term: { category: filters.category } });
    }

    if (filters.minPrice || filters.maxPrice) {
        filter.push({
            range: {
                price: {
                    ...(filters.minPrice && { gte: filters.minPrice }),
                    ...(filters.maxPrice && { lte: filters.maxPrice }),
                },
            },
        });
    }

    const result = await client.search({
        index: 'products',
        body: {
            from,
            size,
            query: { bool: { must, filter } },
            highlight: {
                fields: { name: {}, description: {} },
            },
        },
    });

    const hits  = result.hits.hits;
    const total = result.hits.total.value;

    return {
        data: hits.map(h => ({ id: h._id, ...h._source, highlight: h.highlight })),
        total,
    };
}

module.exports = { client, searchProducts };

Indexing & Deleting Elasticsearch Documents

Keep your Elasticsearch index in sync whenever a product is created, updated, or deleted in the database.

const { client } = require('../lib/elastic');

// Index (upsert) a product
async function indexProduct(product) {
    await client.index({
        index: 'products',
        id:    String(product.id),
        document: {
            name:        product.name,
            description: product.description,
            category:    product.category,
            price:       product.price,
            tags:        product.tags || [],
            created_at:  product.created_at,
        },
    });
    // Refresh immediately (dev only – use refresh:false in production)
    await client.indices.refresh({ index: 'products' });
}

// Remove a product from the index
async function removeProduct(productId) {
    await client.delete({ index: 'products', id: String(productId) });
}

module.exports = { indexProduct, removeProduct };