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!