Pagination — API

Pagination membagi data besar menjadi halaman-halaman kecil. Query parameter pagination: GET /products?page=2&limit=10 Response dengan metadata: { "data"…

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
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}`,
    },
  });
});
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"
  }
}

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

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.

Yang akan kamu pelajari