Pagination artinya membagi daftar panjang jadi halaman-halaman kecil, dan client meminta satu halaman per request. Bayangin katalog cetak Toko Buku. Gak ada yang mencetak 20.000 buku di satu lembar; katalognya dibagi per halaman, ada nomor halamannya, dan di bawah tiap halaman tertulis "halaman 2 dari 4". API juga begitu: GET /books gak mengirim semua buku sekaligus, tapi 5 dulu, lalu 5 berikutnya kalau diminta.
🧪 Tebakan cepat: kamu membuka halaman 1 (
?page=1&limit=5, buku nomor 1 sampai 5). Sebelum kamu membuka halaman 2, admin menghapus buku nomor 3. Halaman 2 dimulai dari buku nomor berapa? Tebak dulu, jawabannya ada di bagian "Offset vs cursor".
Kenapa dibagi per halaman
Katalog Toko Buku sekarang berisi 20 buku. Nanti bisa 20.000. Mengirim semuanya di satu response bikin response-nya besar, server-nya berat, dan HP pembeli lambat menampilkannya, padahal yang dilihat cuma 5 teratas. Pagination menjaga performa: tiap request cuma membawa satu halaman.
page dan limit di query string
Client memilih halamannya lewat query string, seperti di pelajaran URL & Endpoint:
GET /books?page=2&limit=5
pageadalah nomor halaman, mulai dari 1.limitadalah jumlah buku per halaman.
app.get("/books", (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 5;
const start = (page - 1) * limit;
const data = books.slice(start, start + limit);
const total = books.length;
const totalPages = Math.ceil(total / limit);
res.json({
data,
meta: { page, limit, total, totalPages },
links: {
first: `/books?page=1&limit=${limit}`,
prev: page > 1 ? `/books?page=${page - 1}&limit=${limit}` : null,
next: page < totalPages ? `/books?page=${page + 1}&limit=${limit}` : null,
last: `/books?page=${totalPages}&limit=${limit}`,
},
});
});
req.query.pageitu teks ("2"), sama sepertireq.params.id.parseIntmengubahnya jadi angka, dan|| 1memberi nilai bawaan kalau gak dikirim (parseInt(undefined)hasilnyaNaN, danNaN || 1jadi1).start = (page - 1) * limitadalah posisi buku pertama di halaman itu. Halaman 1 mulai dari indeks 0.books.slice(start, start + limit)memotong array daristartsampai sebelumstart + limit.Math.ceilmembulatkan ke atas, karena sisa yang gak penuh tetap butuh satu halaman.
page |
start |
slice |
Buku |
|---|---|---|---|
| 1 | 0 | slice(0, 5) |
1 sampai 5 |
| 2 | 5 | slice(5, 10) |
6 sampai 10 |
| 4 | 15 | slice(15, 20) |
16 sampai 20 |
| 5 | 20 | slice(20, 25) |
gak ada: data kosong, tetap 200 |
Jumlah halamannya: 20 buku dibagi 5 = 4 halaman. Kalau totalnya 25 dan per halaman 10, hasilnya 2,5, dibulatkan ke atas jadi 3 halaman, dan halaman terakhir berisi 5.
meta dan links
Response halaman 2, dirapikan:
{
"data": [
{ "id": 6, "title": "Edensor" },
{ "id": 7, "title": "Anak Semua Bangsa" },
{ "id": 8, "title": "Lelaki Harimau" },
{ "id": 9, "title": "Negeri 5 Menara" },
{ "id": 10, "title": "Perahu Kertas" }
],
"meta": { "page": 2, "limit": 5, "total": 20, "totalPages": 4 },
"links": {
"first": "/books?page=1&limit=5",
"prev": "/books?page=1&limit=5",
"next": "/books?page=3&limit=5",
"last": "/books?page=4&limit=5"
}
}
dataadalah isi halamannya.metamenjawab "aku di mana": halaman berapa, berapa per halaman, total berapa, ada berapa halaman. Frontend menulis "Halaman 2 dari 4" dari sini.linksadalah alamat siap pakai. Tombol "Berikutnya" cukup memanggillinks.next; kalau isinyanull, berarti sudah halaman terakhir.
Di Laravel, satu baris Book::paginate(5) membaca ?page= sendiri dan mengembalikan data plus info yang sama (current_page, per_page, total, last_page, next_page_url, dan lainnya).
Offset vs cursor
Cara di atas disebut offset pagination: "lewati sekian buku, ambil sekian". Masalahnya muncul kalau datanya berubah di antara dua request.
Kamu membuka halaman 1: buku 1 sampai 5. Lalu buku nomor 3 dihapus. Sekarang di posisi 5 sampai 9 ada buku 7 sampai 11, jadi halaman 2 mulai dari buku 7, dan buku 6 gak pernah kamu lihat. Itu jawaban tebakannya. Kebalikannya juga terjadi: di daftar "terbaru dulu", buku baru yang masuk di depan menggeser semuanya, dan buku terakhir di halaman 1 muncul dobel di awal halaman 2.
Cursor pagination gak menghitung posisi. Dia mengingat buku terakhir yang sudah kamu lihat, lalu meminta "5 buku sesudah nomor 5":
const after = parseInt(req.query.after) || 0;
const rest = books.filter((b) => b.id > after);
const data = rest.slice(0, limit);
const nextCursor = rest.length > limit ? data[data.length - 1].id : null;
GET /books?limit=5&after=5 tetap mulai dari buku 6, walau buku 3 sudah dihapus. Gak ada yang terlewat, gak ada yang dobel.
Offset (?page=2) |
Cursor (?after=5) |
|
|---|---|---|
| Lompat ke halaman 7 | bisa | gak bisa, harus berurutan |
| "Halaman 2 dari 4" | bisa | biasanya gak ada total |
| Data berubah di tengah jalan | bisa terlewat atau dobel | stabil |
| Cocok untuk | tabel admin, hasil pencarian bernomor | feed, infinite scroll, riwayat chat |
Di Laravel, cursor pagination ditulis Book::cursorPaginate(5). Cursor-nya berupa teks acak, bukan id polos, tapi idenya sama.
⚠️ Jebakan yang sering ketemu
- Lupa
parseInt."2" * 5kebetulan jadi 10, tapi"2" + 5jadi"25". Ubahpagedanlimitjadi angka di awal. - Halaman dimulai dari 0.
start = page * limitbikin halaman 1 melewati 5 buku pertama. Tulis(page - 1) * limit. - Limit tanpa batas.
?limit=1000000mengembalikan semuanya lagi. Batasi, misalnyaMath.min(limit, 100). - Page negatif.
?page=-1bikinstartnegatif, danslice(-10, -5)mengambil dari belakang. Pastikanpageminimal 1.
TL;DR: Pagination membagi daftar jadi halaman supaya response tetap ringan (performa). Offset: ?page=2&limit=5, parseInt(req.query.page) || 1, start = (page - 1) * limit, books.slice(start, start + limit), dan totalPages = Math.ceil(total / limit) (25 data per 10 = 3 halaman). Response-nya { data, meta: { page, limit, total, totalPages }, links: { first, prev, next, last } }; halaman kosong tetap 200. Di Laravel: Book::paginate(5). Offset bisa melewatkan atau mendobelkan data kalau datanya berubah di antara request; cursor (?after=5, atau cursorPaginate di Laravel) melanjutkan dari item terakhir yang dilihat, jadi stabil, tapi gak bisa lompat ke halaman tertentu.