REST API lapozás és rendezés egységes kialakítása backendben.

REST API lapozás, szűrés és rendezés: hogyan legyen következetes a kialakítás?

Üdv a fedélzeten, fejlesztőtárs! Ma egy olyan témát boncolgatunk, amely első ránézésre egyszerűnek tűnik, de gyakran vált kaotikussá a valós projektekben: a REST API-k lapozásának, szűrésének és rendezésének következetes kialakítása. Gondolj csak bele, hányszor kaptál már olyan API választ, ahol a lapozás logikája máshogy működik, mint a szűrés, vagy a rendezési paraméterek neve kontextusonként változik. Ezen a blogon keresztül végigjárjuk, hogyan tervezzünk meg egy átlátható, karbantartható és – ami a legfontosabb – következetes rendszert ezekhez a alapvető funkciókhoz.

Miért fontos a következetesség?

Mielőtt belevágunk a technikai részletekbe, álljunk meg egy pillanatra: miért is foglalkozunk ezzel annyira? Egy következetes API nem csak a frontend fejlesztők életét könnyíti meg. Egyértelmű konvenciók, kiszámítható viselkedés és egységes hibakezelés jelentősen csökkenti az integrációs időt, javítja a fejlesztői élményt (DX) és hosszú távon könnyebbé teszi az API bővítését és karbantartását. Egy jól megtervezett lekérdezői felület gyakorlatilag öndokumentáló.

Az alapok: paraméterek és konvenciók

Kezdjük a legalapvetőbbel: milyen query paramétereket használjunk? Ajánlom, hogy ragaszkodj egységes elnevezésekhez az egész API-ban. Íme egy bevált minta:

Lapozás: page (aktuális oldal), limit vagy per_page (elemek száma oldalanként) – Rendezés: sort (mező neve), order vagy direction (asc vagy desc) – Szűrés: filter[mezo_neve] vagy mezo_neve (konkrét érték vagy operátorral)

A lényeg, hogy válaszd ki a struktúrát, és tartsd ki minden végpontnál. A szűrésnél különösen fontos eldönteni, támogatni fogsz-e összetett operátorokat (pl. filter[ar][gt]=1000).

Backend implementáció PHP-ban

Nézzünk egy egyszerű, de hatékony PHP backend példát, amely kezeli ezeket a paramétereket. Tegyük fel, hogy egy terméklistát kérdezünk le.

<?php
// products.php - Példa végpont lapozással, szűréssel, rendezéssel

class ProductController {
    public function getProducts(Request $request) {
        // Paraméterek kinyerése egységes konvenciókkal
        $page = max(1, (int) ($request->query['page'] ?? 1));
        $limit = min(100, max(1, (int) ($request->query['limit'] ?? 20)));
        $sortField = $request->query['sort'] ?? 'created_at';
        $sortOrder = strtolower($request->query['order'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
        
        // Szűrési paraméterek gyűjtése (egyszerű egyenlőség alapján itt)
        $filters = [];
        foreach ($request->query as $key => $value) {
            if (strpos($key, 'filter_') === 0) {
                $fieldName = substr($key, 7); // "filter_" előtag eltávolítása
                $filters[$fieldName] = $value;
            }
        }
        
        // Offset számítás
        $offset = ($page - 1) * $limit;
        
        // Dinamikus SQL (PDO példa, valós alkalmazásban inkább Query Buildert használj)
        $sql = "SELECT * FROM products WHERE 1=1";
        $params = [];
        
        foreach ($filters as $field => $value) {
            // EGYÉNI FIGYELMEZTETÉS: Itt mindenképpen validáld a mezőneveket, hogy ne legyen SQL injection!
            if (in_array($field, ['category_id', 'active', 'price'])) { // Whitelist
                $sql .= " AND $field = ?";
                $params[] = $value;
            }
        }
        
        // Rendezés hozzáadása (szintén whitelist fontos!)
        $allowedSortFields = ['created_at', 'price', 'name'];
        $sortField = in_array($sortField, $allowedSortFields) ? $sortField : 'created_at';
        $sql .= " ORDER BY $sortField $sortOrder";
        
        // Lapozás hozzáadása
        $sql .= " LIMIT ? OFFSET ?";
        $params[] = $limit;
        $params[] = $offset;
        
        // Lekérdezés végrehajtása (PDO példa)
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        // Összes elem számának lekérdezése a lapozáshoz
        $countSql = "SELECT COUNT(*) as total FROM products WHERE 1=1";
        // ... itt ugyanazok a szűrők jönnének, egyszerűsítés kedvéért kihagyjuk
        $total = $pdo->query($countSql)->fetchColumn();
        $totalPages = ceil($total / $limit);
        
        // Válasz összeállítása következetes struktúrával
        return [
            'data' => $products,
            'pagination' => [
                'current_page' => $page,
                'per_page' => $limit,
                'total_items' => $total,
                'total_pages' => $totalPages,
                'has_next_page' => $page < $totalPages,
                'has_prev_page' => $page > 1
            ],
            'filters' => $filters, // Visszaadjuk, hogy tudja a frontend, mi alapján szűrt
            'sort' => [
                'field' => $sortField,
                'order' => $sortOrder
            ]
        ];
    }
}
?>

Frontend: jQuery és Bootstrap integráció

A backend következetessége csak akkor nyeri el teljes értékét, ha a frontend is könnyedén tudja kezelni az API-t. Nézzünk egy példát, ahol jQuery-val és Bootstrap komponensekkel készítünk egy felhasználóbarát interfészt.

// products.js - API hívás kezelése a paraméterekkel
function loadProducts(page = 1) {
    // Paraméterek összeállítása a form alapján
    var params = {
        page: page,
        limit: $('#perPageSelect').val(),
        sort: $('#sortFieldSelect').val(),
        order: $('#sortOrderSelect').val()
    };
    
    // Szűrők hozzáadása (filter_ előtaggal, ahogy a backend várja)
    $('.filter-input').each(function() {
        var fieldName = $(this).data('field');
        var value = $(this).val();
        if (value) {
            params['filter_' + fieldName] = value;
        }
    });
    
    // API hívás
    $.ajax({
        url: '/api/products',
        data: params,
        method: 'GET',
        success: function(response) {
            renderProducts(response.data);
            renderPagination(response.pagination);
            updateActiveFilters(response.filters);
        },
        error: function(xhr) {
            $('#errorAlert').text('Hiba történt a betöltés során.').show();
        }
    });
}

// Példa a lapozás renderelésére Bootstrap pagination komponenssel
function renderPagination(paginationInfo) {
    var $pagination = $('#productPagination');
    $pagination.empty();
    
    if (paginationInfo.total_pages <= 1) return;
    
    // Előző oldal gomb
    var prevClass = paginationInfo.has_prev_page ? '' : ' disabled';
    $pagination.append('<li class="page-item' + prevClass + '"><a class="page-link" href="#" data-page="' + (paginationInfo.current_page - 1) + '">Előző</a></li>');
    
    // Oldalszámok
    for (var i = 1; i <= paginationInfo.total_pages; i++) {
        var activeClass = i === paginationInfo.current_page ? ' active' : '';
        $pagination.append('<li class="page-item' + activeClass + '"><a class="page-link" href="#" data-page="' + i + '">' + i + '</a></li>');
    }
    
    // Következő oldal gomb
    var nextClass = paginationInfo.has_next_page ? '' : ' disabled';
    $pagination.append('<li class="page-item' + nextClass + '"><a class="page-link" href="#" data-page="' + (paginationInfo.current_page + 1) + '">Következő</a></li>');
    
    // Eseménykezelő
    $pagination.find('a.page-link').click(function(e) {
        e.preventDefault();
        loadProducts($(this).data('page'));
    });
}
// products.scss - A komponens stílusai
#productTable {
    margin-top: 2rem;
    
    .sort-header {
        cursor: pointer;
        user-select: none;
        position: relative;
        padding-right: 1.5rem !important;
        
        &:hover {
            background-color: #f8f9fa;
        }
        
        &.asc:after {
            content: '↑';
            position: absolute;
            right: 0.5rem;
        }
        
        &.desc:after {
            content: '↓';
            position: absolute;
            right: 0.5rem;
        }
    }
}

.filter-panel {
    background-color: #f8f9fa;
    border-radius: 0.375rem;
    padding: 1.5rem;
    margin-bottom: 2rem;
    
    .filter-group {
        margin-bottom: 1rem;
        
        label {
            font-weight: 600;
            margin-bottom: 0.5rem;
            display: block;
            color: #495057;
        }
    }
}

.pagination-container {
    display: flex;
    justify-content: center;
    margin-top: 2rem;
}

Gyakori buktatók és tanácsok

1. Offset vs. Cursor-based lapozás: Nagy adathalmazoknál az offset (LIMIT 20 OFFSET 1000) lassú lehet. Érdemes megfontolni a cursor-based megközelítést (pl. WHERE id > ? LIMIT 20), ha a teljesítmény kritikus.

2. Metaadatok visszaadása: Ne felejtsd el visszaadni a lapozással, szűréssel kapcsolatos metaadatokat a válaszban. Így a frontend tudja megjeleníteni, hogy „1-20 / 256 találat”.

3. Paraméter validáció: Mindig validáld és sanitizáld a bejövő paramétereket, különösen a rendezési mezőneveket és a szűrési értékeket.

4. Alapértelmezett rendezés: Mindig állíts be egy alapértelmezett rendezési sorrendet (pl. created_at DESC), hogy az adatok mindig kiszámítható sorrendben érkezzenek.

5. Válasz struktúra: Legyen következetes a válasz strukturája minden végponton. Ha az egyiknél a lapozási adatok a meta objektumban vannak, a másiknál ne legyenek a gyökérszinten.

Összegzés

Egy következetesen megtervezett REST API lekérdezői felület – a lapozás, szűrés és rendezés egységes kialakításával – nem csak technikai megoldás, hanem a fejlesztői kommunikáció alapja. A fenti példák bemutatták, hogyan implementálhatod PHP backend oldalon, és hogyan használhatod fel jQuery és Bootstrap segítségével frontend oldalon. Emlékezz: a kulcs a konvenciók kialakításában és követésében rejlik. Ha ezt az elejétől fogod tudatosan építeni, hosszú távon rengeteg fejfájástól és refactoringtól kímélheted meg magad és a csapatod.

Tartsd szem előtt, hogy minden projekttől függően változhatnak a konkrét igények, de az alapelvek – következetesség, átláthatóság, kiszámíthatóság – mindig érvényesek maradnak. Kódolj okosan, és legyenek könnyen integrálható API-aid!