Filtering va Sorting — query params bilan

~15 daq · Express · API Arxitekturasi

Muammodan boshlaylik

Foydalanuvchi mahsulotlar ro'yxatini ko'ryapti. U istaydi:

  • Faqat elektronika kategoriyasidagi mahsulotlar
  • Narxi 50,000 dan 200,000 gacha
  • Narz bo'yicha arzondan qimmatlga tartiblangan

Buning uchun alohida endpoint yozish — noto'g'ri:

// ❌ — har holat uchun alohida endpoint
app.get('/products/elektronika', ...);
app.get('/products/elektronika/cheap', ...);
app.get('/products/elektronika/expensive', ...);
// ... cheksiz kombinatsiyalar

To'g'ri yondashuv: bitta endpoint, query params orqali filter va sort:

GET /products?category=elektronika&minPrice=50000&maxPrice=200000&sort=price&order=asc

Arxitektura

Filtr va sort logikasini route ichiga yozmaslik kerak. Uni alohida funksiyaga ajratamiz:

routes/
  products.js        ← faqat route va response
utils/
  buildQuery.js      ← filter + sort logikasi

Bu nima beradi? Route toza qoladi. Logika qayta ishlatiladi. Test qilish oson.


1-qadam: buildQuery yordamchi funksiyasi

// utils/buildQuery.js

const buildQuery = (queryParams) => {
  const { category, minPrice, maxPrice, search } = queryParams;

  const filter = {}; // ← Mongoose ga beriladigan filter

  // Kategoriya bo'yicha
  if (category) {
    filter.category = category; // ← { category: "elektronika" }
  }

  // Narx oralig'i
  if (minPrice || maxPrice) {
    filter.price = {};
    if (minPrice) filter.price.$gte = Number(minPrice); // ← kattaroq yoki teng
    if (maxPrice) filter.price.$lte = Number(maxPrice); // ← kichik yoki teng
  }

  // Matn qidirish
  if (search) {
    filter.name = { $regex: search, $options: 'i' }; // ← katta-kichik harfsiz
  }

  return filter;
};

module.exports = buildQuery;

2-qadam: buildSort yordamchi funksiyasi

// utils/buildSort.js

const ALLOWED_SORT_FIELDS = ['price', 'name', 'createdAt']; // ← faqat shu maydonlar

const buildSort = (queryParams) => {
  const { sort = 'createdAt', order = 'desc' } = queryParams;

  // Ruxsatsiz maydonga sort qilishni oldini olish
  const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : 'createdAt';
  const sortOrder = order === 'asc' ? 1 : -1; // ← Mongoose: 1 yoki -1

  return { [sortField]: sortOrder };
};

module.exports = buildSort;

Nima uchun ALLOWED_SORT_FIELDS? Foydalanuvchi sort=password yuborsa nima bo'ladi? Yoki sort=__v? Faqat ruxsat etilgan maydonlar ishlashi kerak.


3-qadam: Route — toza va sodda

// routes/products.js
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');
const buildQuery = require('../utils/buildQuery');
const buildSort = require('../utils/buildSort');

router.get('/', async (req, res, next) => {
  try {
    const filter = buildQuery(req.query);  // ← filter logikasi tashqarida
    const sort   = buildSort(req.query);   // ← sort logikasi tashqarida

    const products = await Product.find(filter).sort(sort);

    res.json({ success: true, data: products });
  } catch (err) {
    next(err);
  }
});

module.exports = router;

Route ichida hech qanday if yo'q — toza.


Pagination bilan birga

// routes/products.js
const buildQuery = require('../utils/buildQuery');
const buildSort  = require('../utils/buildSort');

router.get('/', async (req, res, next) => {
  try {
    const filter = buildQuery(req.query);
    const sort   = buildSort(req.query);

    const page  = Math.max(1, parseInt(req.query.page)  || 1);
    const limit = Math.min(100, parseInt(req.query.limit) || 10);
    const skip  = (page - 1) * limit;

    const [products, total] = await Promise.all([
      Product.find(filter).sort(sort).skip(skip).limit(limit),
      Product.countDocuments(filter), // ← filtrlangan umumiy son
    ]);

    res.json({
      success: true,
      data: products,
      meta: {
        page, limit, total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (err) {
    next(err);
  }
});

So'rovlar misoli:

GET /products
GET /products?category=elektronika
GET /products?minPrice=50000&maxPrice=200000
GET /products?search=iphone
GET /products?sort=price&order=asc
GET /products?category=elektronika&sort=price&order=asc&page=2&limit=20

Hammasi bitta endpoint — kombinatsiyalar cheksiz.


SQL (PostgreSQL) da xuddi shu yondashuv

// utils/buildSqlQuery.js

const buildSqlQuery = (queryParams) => {
  const { category, minPrice, maxPrice, search } = queryParams;

  const conditions = []; // ← WHERE shartlari
  const values     = []; // ← parametrlar (injection himoya)

  if (category) {
    values.push(category);
    conditions.push(`category = ${values.length}`);
  }

  if (minPrice) {
    values.push(Number(minPrice));
    conditions.push(`price >= ${values.length}`);
  }

  if (maxPrice) {
    values.push(Number(maxPrice));
    conditions.push(`price <= ${values.length}`);
  }

  if (search) {
    values.push(`%${search}%`);
    conditions.push(`name ILIKE ${values.length}`); // ← katta-kichik harfsiz
  }

  const where = conditions.length > 0
    ? 'WHERE ' + conditions.join(' AND ')
    : '';

  return { where, values };
};

module.exports = buildSqlQuery;
// routes/products.js (SQL versiya)
const buildSqlQuery = require('../utils/buildSqlQuery');

router.get('/', async (req, res, next) => {
  try {
    const { where, values } = buildSqlQuery(req.query);

    const { sort = 'created_at', order = 'desc' } = req.query;
    const ALLOWED = ['price', 'name', 'created_at'];
    const sortField = ALLOWED.includes(sort) ? sort : 'created_at';
    const sortOrder = order === 'asc' ? 'ASC' : 'DESC';

    const query = `SELECT * FROM products ${where} ORDER BY ${sortField} ${sortOrder}`;
    const result = await pool.query(query, values);

    res.json({ success: true, data: result.rows });
  } catch (err) {
    next(err);
  }
});

To'liq papka tuzilishi

project/
├── routes/
│   └── products.js      ← GET / endpoint, response
├── utils/
│   ├── buildQuery.js    ← MongoDB filter
│   ├── buildSort.js     ← sort logikasi
│   └── response.js      ← success/error yordamchilari (37-dars)
├── models/
│   └── Product.js
└── app.js

Tez-tez uchraydigan xatolar

1. Number ga o'tkazmaslik

// ❌ — query params har doim string bo'lib keladi
filter.price = { $gte: req.query.minPrice }; // ← "50000" (string)
// MongoDB string va raqamni taqqoslaydi — noto'g'ri natija
// ✅
filter.price = { $gte: Number(req.query.minPrice) }; // ← 50000 (number)

2. Sort maydonini tekshirmaslik

// ❌ — foydalanuvchi istalgan maydonga sort qila oladi
const sort = { [req.query.sort]: -1 };
// ✅ — whitelist bilan
const ALLOWED = ['price', 'name', 'createdAt'];
const sortField = ALLOWED.includes(req.query.sort) ? req.query.sort : 'createdAt';

3. Bo'sh search bilan regex

// ❌ — search="" bo'lsa, barcha yozuvlarni regex bilan skanerlaydi, sekin
if (search) {
  filter.name = { $regex: search, $options: 'i' };
}
// search = "" ham truthy... yo'q, aslida "" falsy. Lekin "  " (bo'shliq) truthy
// ✅ — trim() bilan tozalash
const search = req.query.search?.trim();
if (search) {
  filter.name = { $regex: search, $options: 'i' };
}

Xulosa

  • Filter va sort logikasini route dan ajrating — buildQuery.js, buildSort.js
  • Route toza qoladi: find(filter).sort(sort) — ikki qator
  • Sort maydonlarini whitelist bilan tekshiring — xavfsizlik
  • Query params string bo'lib keladi — Number() bilan o'tkazing
  • Pagination bilan birga: countDocuments(filter) — filtrlangan umumiy son

Keyingi dars: 40-dars: API dokumentatsiya — Swagger bilan