Cara Implementasi Pagination di REST API: Panduan Lengkap

Mark Ponomarev

Mark Ponomarev

19 May 2025

Cara Implementasi Pagination di REST API: Panduan Lengkap

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 alat Pengujian API yang hebat yang menghasilkan Dokumentasi API yang indah?

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!
button

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

Sebagai alternatif, klien mungkin menentukan:

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)
atau
GET /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:

Kekurangan Offset/Limit Pagination:

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:

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:

Kekurangan Cursor-Based Pagination:

Memilih Strategi yang Tepat

Pilihan antara pagination offset/limit dan berbasis kursor bergantung pada kebutuhan spesifik Anda:

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:

  1. 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 atau snake_case) di seluruh API Anda.
  2. Sediakan Tautan Navigasi (HATEOAS): Seperti yang ditunjukkan dalam contoh respons, sertakan tautan untuk self, next, prev, first, dan last (jika berlaku). Ini membuat API lebih mudah ditemukan dan melepaskan klien dari logika pembangunan URL.
  3. Nilai Default dan Batas Maksimum:
  1. Dokumentasi API yang Jelas: Dokumentasikan strategi pagination Anda secara menyeluruh:
  1. 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.
  2. Tangani Kasus Khusus:
  1. Pertimbangan Jumlah Total:
  1. Penanganan Kesalahan: Kembalikan kode status HTTP yang sesuai untuk kesalahan (misalnya, 400 untuk input buruk, 500 untuk kesalahan server saat mengambil data).
  2. 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.
  3. 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)

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.


Mengembangkan API dengan Apidog

Apidog adalah alat pengembangan API yang membantu Anda mengembangkan API dengan lebih mudah dan efisien.