Saat membangun REST API yang mengembalikan daftar sumber daya, sangat penting untuk mempertimbangkan cara menangani dataset besar. Mengembalikan ribuan atau bahkan jutaan catatan dalam satu respons API tidak praktis dan dapat menyebabkan masalah kinerja yang signifikan, konsumsi memori yang tinggi baik untuk server maupun klien, dan pengalaman pengguna yang buruk. Pagination adalah solusi standar untuk masalah ini. Ini melibatkan pemecahan dataset besar menjadi bagian-bagian yang lebih kecil dan mudah dikelola yang disebut "halaman", yang kemudian disajikan secara berurutan. Tutorial ini akan memandu Anda melalui langkah-langkah teknis implementasi berbagai strategi pagination dalam REST API Anda.
Ingin platform Terintegrasi, All-in-One untuk Tim Pengembang Anda bekerja sama dengan produktivitas maksimum?
Apidog memenuhi semua kebutuhan Anda, dan menggantikan Postman dengan harga yang jauh lebih terjangkau!
Mengapa Pagination Penting?
Sebelum masuk ke detail implementasi, mari kita singgung secara singkat mengapa pagination adalah fitur yang tidak bisa dinegosiasikan untuk API yang berurusan dengan kumpulan sumber daya:
- Kinerja: Meminta dan mentransfer data dalam jumlah besar bisa lambat. Pagination mengurangi ukuran payload setiap permintaan, menghasilkan waktu respons yang lebih cepat dan beban server yang berkurang.
- Konsumsi Sumber Daya: Respons yang lebih kecil mengonsumsi lebih sedikit memori di server yang menghasilkannya dan di klien yang menguraikannya. Ini sangat penting untuk klien seluler atau lingkungan dengan sumber daya terbatas.
- Pembatasan Tingkat dan Kuota: Banyak API memberlakukan pembatasan tingkat (rate limits). Pagination membantu klien tetap dalam batas ini dengan mengambil data dalam potongan-potongan yang lebih kecil dari waktu ke waktu, daripada mencoba mendapatkan semuanya sekaligus.
- Pengalaman Pengguna: Untuk UI yang mengonsumsi API, menyajikan data dalam halaman jauh lebih ramah pengguna daripada membanjiri pengguna dengan daftar yang sangat besar atau gulir yang sangat panjang.
- Efisiensi Basis Data: Mengambil subset data umumnya kurang membebani basis data dibandingkan dengan mengambil seluruh tabel, terutama jika pengindeksan yang tepat sudah ada.
Strategi Pagination Umum
Ada beberapa strategi umum untuk mengimplementasikan pagination, masing-masing dengan kelebihan dan kekurangannya sendiri. Kita akan menjelajahi yang paling populer: offset/limit (sering disebut sebagai berbasis halaman) dan berbasis kursor (juga dikenal sebagai keyset atau seek pagination).
1. Offset/Limit (atau Berbasis Halaman) Pagination
Ini bisa dibilang metode pagination yang paling mudah dan paling banyak diadopsi. Cara kerjanya adalah dengan memungkinkan klien menentukan dua parameter utama:
offset
: Jumlah catatan yang harus dilewati dari awal dataset.limit
: Jumlah maksimum catatan yang akan dikembalikan dalam satu halaman.
Sebagai alternatif, klien mungkin menentukan:
page
: Nomor halaman yang ingin mereka ambil.pageSize
(atauper_page
,limit
): Jumlah catatan per halaman.
offset
dapat dihitung dari page
dan pageSize
menggunakan rumus: offset = (page - 1) * pageSize
.
Langkah-langkah Implementasi Teknis:
Mari kita asumsikan kita memiliki endpoint API /items
yang mengembalikan daftar item.
a. Parameter Permintaan API:
Klien akan membuat permintaan seperti:GET /items?offset=20&limit=10
(ambil 10 item, lewati 20 item pertama)
atauGET /items?page=3&pageSize=10
(ambil halaman ke-3, dengan 10 item per halaman, yang setara dengan offset=20, limit=10).
Merupakan praktik yang baik untuk menetapkan nilai default untuk parameter ini (misalnya, limit=20
, offset=0
atau page=1
, pageSize=20
) jika klien tidak menyediakannya. Juga, terapkan batas maksimum limit
atau pageSize
untuk mencegah klien meminta jumlah catatan yang terlalu besar, yang dapat membebani server.
b. Logika Backend (Konseptual):
Ketika server menerima permintaan ini, ia perlu menerjemahkan parameter ini ke dalam kueri basis data.
// Contoh dalam Java dengan Spring Boot
@GetMapping("/items")
public ResponseEntity<PaginatedResponse<Item>> getItems(
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "20") int limit
) {
// Validasi limit untuk mencegah penyalahgunaan
if (limit > 100) {
limit = 100; // Terapkan batas maksimum
}
List<Item> items = itemRepository.findItemsWithOffsetLimit(offset, limit);
long totalItems = itemRepository.countTotalItems(); // Untuk metadata
// Buat dan kembalikan respons paginasi
// ...
}
c. Kueri Basis Data (Contoh SQL):
Sebagian besar basis data relasional mendukung klausa offset dan limit secara langsung.
Untuk PostgreSQL atau MySQL:
SELECT *
FROM items
ORDER BY created_at DESC -- Pengurutan yang konsisten sangat penting untuk pagination yang stabil
LIMIT 10 -- Ini adalah parameter 'limit'
OFFSET 20; -- Ini adalah parameter 'offset'
Untuk SQL Server (versi lama mungkin menggunakan ROW_NUMBER()
):
SELECT *
FROM items
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
Untuk Oracle:
SELECT *
FROM (
SELECT i.*, ROWNUM rnum
FROM (
SELECT *
FROM items
ORDER BY created_at DESC
) i
WHERE ROWNUM <= 20 + 10 -- offset + limit
)
WHERE rnum > 20; -- offset
Catatan Penting tentang Pengurutan: Agar pagination offset/limit dapat diandalkan, dataset yang mendasarinya harus diurutkan berdasarkan kunci yang konsisten dan unik (atau hampir unik), atau kombinasi kunci. Jika urutan item dapat berubah antar permintaan (misalnya, item baru dimasukkan atau item diperbarui dengan cara yang memengaruhi urutan pengurutannya), pengguna mungkin melihat item duplikat atau kehilangan item saat menavigasi halaman. Pilihan umum adalah mengurutkan berdasarkan stempel waktu pembuatan atau ID utama.
d. Struktur Respons API:
Respons paginasi yang baik tidak hanya mencakup data untuk halaman saat ini tetapi juga metadata untuk membantu klien menavigasi.
{
"data": [
// array item untuk halaman saat ini
{ "id": "item_21", "name": "Item 21", ... },
{ "id": "item_22", "name": "Item 22", ... },
// ... hingga 'limit' item
{ "id": "item_30", "name": "Item 30", ... }
],
"pagination": {
"offset": 20,
"limit": 10,
"totalItems": 5000, // Jumlah total item yang tersedia
"totalPages": 500, // Dihitung sebagai ceil(totalItems / limit)
"currentPage": 3 // Dihitung sebagai (offset / limit) + 1
},
"links": { // Tautan HATEOAS untuk navigasi
"self": "/items?offset=20&limit=10",
"first": "/items?offset=0&limit=10",
"prev": "/items?offset=10&limit=10", // Null jika di halaman pertama
"next": "/items?offset=30&limit=10", // Null jika di halaman terakhir
"last": "/items?offset=4990&limit=10"
}
}
Menyediakan tautan HATEOAS (Hypermedia as the Engine of Application State) (self
, first
, prev
, next
, last
) adalah praktik terbaik REST. Ini memungkinkan klien untuk menavigasi melalui halaman tanpa harus membangun URL sendiri.
Kelebihan Offset/Limit Pagination:
- Kesederhanaan: Mudah dipahami dan diimplementasikan.
- Navigasi Berbasis Status: Memungkinkan navigasi langsung ke halaman tertentu (misalnya, "lompat ke halaman 50").
- Didukung Luas: Dukungan basis data untuk
OFFSET
danLIMIT
umum.
Kekurangan Offset/Limit Pagination:
- Penurunan Kinerja dengan Offset Besar: Seiring bertambahnya nilai
offset
, basis data mungkin menjadi lebih lambat. Basis data sering kali masih harus memindai semuaoffset + limit
baris sebelum membuangoffset
baris. Ini bisa tidak efisien untuk halaman yang dalam. - Penyimpangan Data/Item Hilang: Jika item baru ditambahkan atau item yang ada dihapus dari dataset saat pengguna melakukan pagination, "jendela" data dapat bergeser. Ini dapat menyebabkan pengguna melihat item yang sama di dua halaman berbeda atau kehilangan item sepenuhnya. Ini sangat bermasalah dengan dataset yang sering diperbarui. Misalnya, jika Anda berada di halaman 2 (item 11-20) dan item baru ditambahkan di awal daftar, ketika Anda meminta halaman 3, apa yang sebelumnya item 21 sekarang menjadi item 22. Anda mungkin kehilangan item baru 21 atau melihat duplikat tergantung pada waktu dan pola penghapusan yang tepat.
2. Pagination Berbasis Kursor (Keyset/Seek)
Pagination berbasis kursor mengatasi beberapa kekurangan offset/limit, terutama kinerja dengan dataset besar dan masalah konsistensi data. Alih-alih mengandalkan offset absolut, ia menggunakan "kursor" yang menunjuk ke item tertentu dalam dataset. Klien kemudian meminta item "setelah" atau "sebelum" kursor ini.
Kursor biasanya berupa string buram yang mengkodekan nilai(nilai) kunci pengurutan dari item terakhir yang diambil di halaman sebelumnya.
Langkah-langkah Implementasi Teknis:
a. Parameter Permintaan API:
Klien akan membuat permintaan seperti:GET /items?limit=10
(untuk halaman pertama)
Dan untuk halaman berikutnya:GET /items?limit=10&after_cursor=opaquestringrepresentinglastitemid
Atau, untuk melakukan pagination mundur (kurang umum tetapi mungkin):GET /items?limit=10&before_cursor=opaquestringrepresentingfirstitemid
Parameter limit
masih menentukan ukuran halaman.
b. Apa Itu Kursor?
Kursor seharusnya:
- Buram bagi klien: Klien tidak perlu memahami struktur internalnya. Ia hanya menerimanya dari satu respons dan mengirimkannya kembali dalam permintaan berikutnya.
- Berdasarkan kolom yang unik dan berurutan: Biasanya, ini adalah ID utama (jika berurutan seperti UUIDv1 atau urutan basis data) atau kolom stempel waktu. Jika satu kolom tidak cukup unik (misalnya, beberapa item dapat memiliki stempel waktu yang sama), kombinasi kolom digunakan (misalnya,
timestamp
+id
). - Dapat Dikodekan dan Didekodekan: Sering kali dikodekan Base64 untuk memastikan aman untuk URL. Bisa sesederhana ID itu sendiri, atau objek JSON
{ "last_id": 123, "last_timestamp": "2023-10-27T10:00:00Z" }
yang kemudian dikodekan Base64.
c. Logika Backend (Konseptual):
// Contoh dalam Java dengan Spring Boot
@GetMapping("/items")
public ResponseEntity<CursorPaginatedResponse<Item>> getItems(
@RequestParam(defaultValue = "20") int limit,
@RequestParam(required = false) String afterCursor
) {
// Validasi limit
if (limit > 100) {
limit = 100;
}
// Dekode kursor untuk mendapatkan properti item terakhir yang dilihat
// e.g., LastSeenItemDetails lastSeen = decodeCursor(afterCursor);
// Jika afterCursor null, itu adalah halaman pertama.
List<Item> items;
if (afterCursor != null) {
DecodedCursor decoded = decodeCursor(afterCursor); // e.g., { lastId: "some_uuid", lastCreatedAt: "timestamp" }
items = itemRepository.findItemsAfter(decoded.getLastCreatedAt(), decoded.getLastId(), limit);
} else {
items = itemRepository.findFirstPage(limit);
}
String nextCursor = null;
if (!items.isEmpty() && items.size() == limit) {
// Dengan asumsi item diurutkan, item terakhir dalam daftar digunakan untuk menghasilkan kursor berikutnya
Item lastItemOnPage = items.get(items.size() - 1);
nextCursor = encodeCursor(lastItemOnPage.getCreatedAt(), lastItemOnPage.getId());
}
// Buat dan kembalikan respons paginasi berbasis kursor
// ...
}
// Metode pembantu untuk mengkodekan/mendekode kursor
// private DecodedCursor decodeCursor(String cursor) { ... }
// private String encodeCursor(Timestamp createdAt, String id) { ... }
d. Kueri Basis Data (Contoh SQL):
Kuncinya adalah menggunakan klausa WHERE
yang memfilter catatan berdasarkan kunci pengurutan dari kursor. Klausa ORDER BY
harus selaras dengan komposisi kursor.
Dengan asumsi pengurutan berdasarkan created_at
(menurun) dan kemudian berdasarkan id
(menurun) sebagai pemecah seri untuk pengurutan yang stabil jika created_at
tidak unik:
Untuk halaman pertama:
SELECT *
FROM items
ORDER BY created_at DESC, id DESC
LIMIT 10;
Untuk halaman berikutnya, jika kursor didekode menjadi last_created_at_from_cursor
dan last_id_from_cursor
:
SELECT *
FROM items
WHERE (created_at, id) < (CAST('last_created_at_from_cursor' AS TIMESTAMP), CAST('last_id_from_cursor' AS UUID)) -- Atau tipe yang sesuai
-- Untuk urutan menaik, akan menjadi >
-- Perbandingan tuple (created_at, id) < (val1, val2) adalah cara singkat untuk menulis:
-- WHERE created_at < 'last_created_at_from_cursor'
-- OR (created_at = 'last_created_at_from_cursor' AND id < 'last_id_from_cursor')
ORDER BY created_at DESC, id DESC
LIMIT 10;
Jenis kueri ini sangat efisien, terutama jika ada indeks pada (created_at, id)
. Basis data dapat langsung "mencari" ke titik awal tanpa memindai baris yang tidak relevan.
e. Struktur Respons API:
{
"data": [
// array item untuk halaman saat ini
{ "id": "item_N", "createdAt": "2023-10-27T10:05:00Z", ... },
// ... hingga 'limit' item
{ "id": "item_M", "createdAt": "2023-10-27T10:00:00Z", ... }
],
"pagination": {
"limit": 10,
"hasNextPage": true, // boolean yang menunjukkan apakah ada data lagi
"nextCursor": "base64encodedcursorstringforitem_M" // string buram
// Potensi "prevCursor" jika kursor dua arah didukung
},
"links": {
"self": "/items?limit=10&after_cursor=current_request_cursor_if_any",
"next": "/items?limit=10&after_cursor=base64encodedcursorstringforitem_M" // Null jika tidak ada halaman berikutnya
}
}
Perhatikan bahwa pagination berbasis kursor biasanya tidak menyediakan totalPages
atau totalItems
karena menghitung ini akan memerlukan pemindaian tabel penuh, meniadakan beberapa manfaat kinerja. Jika ini sangat dibutuhkan, endpoint terpisah atau perkiraan mungkin disediakan.
Kelebihan Cursor-Based Pagination:
- Kinerja pada Dataset Besar: Umumnya berkinerja lebih baik daripada offset/limit untuk pagination yang dalam, karena basis data dapat secara efisien mencari posisi kursor menggunakan indeks.
- Stabil dalam Dataset Dinamis: Kurang rentan terhadap item yang hilang atau duplikat ketika data sering ditambahkan atau dihapus, karena kursor berlabuh pada item tertentu. Jika item sebelum kursor dihapus, itu tidak memengaruhi item berikutnya.
- Cocok untuk Infinite Scrolling: Model "halaman berikutnya" sangat cocok dengan UI infinite scroll.
Kekurangan Cursor-Based Pagination:
- Tidak Ada "Lompat ke Halaman": Pengguna tidak dapat langsung menavigasi ke nomor halaman arbitrer (misalnya, "halaman 5"). Navigasi benar-benar berurutan (berikutnya/sebelumnya).
- Implementasi Lebih Kompleks: Mendefinisikan dan mengelola kursor, terutama dengan beberapa kolom pengurutan atau urutan pengurutan yang kompleks, bisa lebih rumit.
- Batasan Pengurutan: Urutan pengurutan harus tetap dan didasarkan pada kolom yang digunakan untuk kursor. Mengubah urutan pengurutan secara langsung dengan kursor itu kompleks.
Memilih Strategi yang Tepat
Pilihan antara pagination offset/limit dan berbasis kursor bergantung pada kebutuhan spesifik Anda:
- Offset/Limit sering kali cukup jika:
- Dataset relatif kecil atau tidak sering berubah.
- Kemampuan untuk melompat ke halaman arbitrer adalah fitur penting.
- Kesederhanaan implementasi adalah prioritas tinggi.
- Kinerja untuk halaman yang sangat dalam bukan masalah utama.
- Cursor-Based umumnya lebih disukai jika:
- Anda berurusan dengan dataset yang sangat besar, sering berubah.
- Kinerja pada skala besar dan konsistensi data selama pagination sangat penting.
- Navigasi berurutan (seperti infinite scrolling) adalah kasus penggunaan utama.
- Anda tidak perlu menampilkan jumlah total halaman atau memungkinkan lompatan ke halaman tertentu.
Dalam beberapa sistem, pendekatan hibrida bahkan digunakan, atau strategi yang berbeda ditawarkan untuk kasus penggunaan atau endpoint yang berbeda.
Praktik Terbaik untuk Mengimplementasikan Pagination
Terlepas dari strategi yang dipilih, patuhi praktik terbaik ini:
- Penamaan Parameter yang Konsisten: Gunakan nama yang jelas dan konsisten untuk parameter pagination Anda (misalnya,
limit
,offset
,page
,pageSize
,after_cursor
,before_cursor
). Patuhi satu konvensi (misalnya,camelCase
atausnake_case
) di seluruh API Anda. - Sediakan Tautan Navigasi (HATEOAS): Seperti yang ditunjukkan dalam contoh respons, sertakan tautan untuk
self
,next
,prev
,first
, danlast
(jika berlaku). Ini membuat API lebih mudah ditemukan dan melepaskan klien dari logika pembangunan URL. - Nilai Default dan Batas Maksimum:
- Selalu tetapkan nilai default yang masuk akal untuk
limit
(misalnya, 10 atau 25). - Terapkan batas maksimum
limit
untuk mencegah klien meminta terlalu banyak data dan membebani server (misalnya, maksimal 100 catatan per halaman). Kembalikan kesalahan atau batasi limit jika nilai yang tidak valid diminta.
- Dokumentasi API yang Jelas: Dokumentasikan strategi pagination Anda secara menyeluruh:
- Jelaskan parameter yang digunakan.
- Sediakan contoh permintaan dan respons.
- Jelaskan batas default dan maksimum.
- Jelaskan cara penggunaan kursor (jika berlaku), tanpa mengungkapkan struktur internalnya jika dimaksudkan untuk buram.
- Pengurutan yang Konsisten: Pastikan data yang mendasarinya diurutkan secara konsisten untuk setiap permintaan paginasi. Untuk offset/limit, ini penting untuk menghindari penyimpangan data. Untuk berbasis kursor, urutan pengurutan menentukan cara kursor dibuat dan diinterpretasikan. Gunakan kolom pemecah seri yang unik (seperti ID utama) jika kolom pengurutan utama dapat memiliki nilai duplikat.
- Tangani Kasus Khusus:
- Hasil Kosong: Kembalikan array data kosong dan metadata pagination yang sesuai (misalnya,
totalItems: 0
atauhasNextPage: false
). - Parameter Tidak Valid: Kembalikan kesalahan
400 Bad Request
jika klien memberikan parameter pagination yang tidak valid (misalnya, limit negatif, nomor halaman bukan integer). - Kursor Tidak Ditemukan (untuk berbasis kursor): Jika kursor yang diberikan tidak valid atau menunjuk ke item yang dihapus, putuskan perilaku: kembalikan
404 Not Found
atau400 Bad Request
, atau kembali ke halaman pertama dengan anggun.
- Pertimbangan Jumlah Total:
- Untuk offset/limit, menyediakan
totalItems
dantotalPages
umum dilakukan. Perlu diingat bahwaCOUNT(*)
bisa lambat pada tabel yang sangat besar. Jelajahi optimasi atau perkiraan khusus basis data jika ini menjadi hambatan. - Untuk berbasis kursor,
totalItems
sering kali dihilangkan demi kinerja. Jika diperlukan, pertimbangkan untuk menyediakan perkiraan jumlah atau endpoint terpisah yang menghitungnya (berpotensi secara asinkron).
- Penanganan Kesalahan: Kembalikan kode status HTTP yang sesuai untuk kesalahan (misalnya,
400
untuk input buruk,500
untuk kesalahan server saat mengambil data). - Keamanan: Meskipun bukan mekanisme pagination secara langsung, pastikan data yang di-paginasi menghormati aturan otorisasi. Pengguna hanya boleh dapat melakukan pagination melalui data yang diizinkan untuk mereka lihat.
- Caching: Respons paginasi sering kali dapat di-cache. Untuk pagination berbasis offset,
GET /items?page=2&pageSize=10
sangat mudah di-cache. Untuk berbasis kursor,GET /items?limit=10&after_cursor=XYZ
juga dapat di-cache. Pastikan strategi caching Anda berfungsi dengan baik dengan cara tautan pagination dibuat dan dikonsumsi. Strategi invalidasi perlu dipertimbangkan jika data yang mendasarinya sering berubah.
Topik Lanjutan (Singgungan Singkat)
- Infinite Scrolling: Pagination berbasis kursor sangat cocok untuk UI infinite scrolling. Klien mengambil halaman pertama, dan saat pengguna menggulir mendekati bagian bawah, ia menggunakan
nextCursor
untuk mengambil set item berikutnya. - Pagination dengan Pemfilteran dan Pengurutan Kompleks: Saat menggabungkan pagination dengan parameter pemfilteran dan pengurutan dinamis, pastikan bahwa:
- Untuk offset/limit: Jumlah
totalItems
secara akurat mencerminkan dataset yang difilter. - Untuk berbasis kursor: Kursor mengkodekan status pengurutan dan pemfilteran jika itu memengaruhi arti "berikutnya". Ini dapat sangat memperumit desain kursor. Seringkali, jika filter atau urutan pengurutan berubah, pagination diatur ulang ke "halaman pertama" dari tampilan baru.
- GraphQL Pagination: GraphQL memiliki cara standarnya sendiri untuk menangani pagination, sering disebut sebagai "Connections". Biasanya menggunakan pagination berbasis kursor dan memiliki struktur yang ditentukan untuk mengembalikan edge (item dengan kursor) dan info halaman. Jika Anda menggunakan GraphQL, patuhi konvensinya (misalnya, spesifikasi Relay Cursor Connections).
Kesimpulan
Mengimplementasikan pagination dengan benar sangat mendasar untuk membangun REST API yang dapat diskalakan dan ramah pengguna. Meskipun pagination offset/limit lebih sederhana untuk memulai, pagination berbasis kursor menawarkan kinerja dan konsistensi yang unggul untuk dataset yang besar dan dinamis. Dengan memahami detail teknis setiap strategi, memilih yang paling sesuai dengan kebutuhan aplikasi Anda, dan mengikuti praktik terbaik untuk implementasi dan desain API, Anda dapat memastikan bahwa API Anda secara efisien mengirimkan data ke klien Anda, berapapun skalanya. Ingatlah untuk selalu memprioritaskan dokumentasi yang jelas dan penanganan kesalahan yang kuat untuk memberikan pengalaman yang mulus bagi konsumen API.