JSONYAMify

Home / Blog / JSON Pagination

Cara Handle Pagination di JSON Response API

Oleh Andi Putra Ogie · Update: Juli 2026 · 9 menit baca

Bayangkan endpoint GET /products yang punya 500.000 baris data. Kalau semuanya dikembalikan sekaligus dalam satu response JSON, selain lambat, juga boros bandwidth dan bisa bikin server maupun client kehabisan memory. Pagination adalah solusi standar: memecah data besar jadi "halaman" kecil yang dikirim bertahap. Artikel ini membahas pola-pola pagination paling umum dipakai di REST API modern, lengkap struktur JSON-nya, kelebihan dan kekurangan masing-masing.

1. Offset-Based Pagination (LIMIT/OFFSET)

Pola paling sederhana dan paling umum ditemui, biasanya lewat query parameter limit dan offset:

GET /products?limit=20&offset=40
{
  "data": [ /* 20 item produk */ ],
  "pagination": {
    "limit": 20,
    "offset": 40,
    "total": 5000
  }
}

Kelebihan: mudah dipahami dan diimplementasi, cocok untuk data yang jarang berubah, memungkinkan lompat langsung ke halaman manapun.
Kekurangan: performa menurun drastis di offset besar karena database tetap harus scan dan skip semua baris sebelum offset (misalnya OFFSET 100000 tetap harus melewati 100.000 baris dulu). Juga rawan bug "data bergeser" — kalau ada insert/delete data baru di antara request halaman 1 dan halaman 2, item bisa terlewat atau muncul dua kali.

2. Page-Based Pagination

Variasi dari offset-based, tapi pakai konsep "nomor halaman" yang lebih intuitif untuk konsumsi manusia (misalnya UI dengan tombol "halaman 1, 2, 3..."):

GET /products?page=3&perPage=20
{
  "data": [ /* 20 item produk */ ],
  "pagination": {
    "currentPage": 3,
    "perPage": 20,
    "totalPages": 250,
    "totalItems": 5000
  }
}

Secara teknis ini cuma offset-based yang dibungkus lebih ramah-user (offset = (page - 1) * perPage). Kelebihan dan kekurangannya sama seperti offset-based di atas.

3. Cursor-Based Pagination

Alih-alih pakai angka offset, cursor-based memakai "penunjuk" (biasanya berupa ID atau token terenkripsi) yang menandai posisi terakhir data yang sudah diambil:

GET /products?limit=20&cursor=eyJpZCI6MTIzfQ==
{
  "data": [ /* 20 item produk */ ],
  "pagination": {
    "nextCursor": "eyJpZCI6MTQzfQ==",
    "hasMore": true
  }
}

Kelebihan: performa tetap konsisten meskipun dataset besar, karena query database langsung "mulai dari sini" tanpa perlu skip baris (biasanya diimplementasikan dengan WHERE id > :cursor_id ORDER BY id LIMIT 20). Juga tidak rawan bug data bergeser seperti offset-based.
Kekurangan: tidak bisa lompat langsung ke halaman tertentu (misalnya "langsung ke halaman 50"), harus jalan berurutan dari cursor sebelumnya. Cocok untuk infinite scroll, kurang cocok untuk UI dengan nomor halaman.

Ini pola yang dipakai API besar seperti Twitter/X, Facebook Graph API, dan Stripe untuk endpoint list dengan dataset besar.

4. Keyset Pagination

Mirip cursor-based, tapi cursor-nya eksplisit berupa nilai kolom asli (biasanya timestamp atau ID), bukan token yang di-encode:

GET /products?limit=20&after_id=143&after_created_at=2026-07-01T10:00:00Z

Query database di baliknya kurang lebih:

SELECT * FROM products
WHERE (created_at, id) > ('2026-07-01T10:00:00Z', 143)
ORDER BY created_at, id
LIMIT 20

Keyset pagination punya keunggulan performa yang sama dengan cursor-based, tapi lebih transparan karena client bisa melihat nilai kolom aslinya (walau ini juga jadi kekurangan dari sisi keamanan/abstraksi — sebagian tim lebih suka menyembunyikannya di balik cursor terenkripsi).

Perbandingan Ringkas

Field Metadata yang Umum Disertakan

Terlepas dari pola mana yang dipilih, beberapa field metadata ini umum ditemukan di response pagination, dan sebaiknya dijadikan standar di seluruh endpoint API kamu supaya client tidak perlu menghafal aturan berbeda-beda untuk tiap resource:

Menyertakan Link Navigasi Langsung (HATEOAS-style)

Beberapa API menyertakan URL lengkap untuk halaman selanjutnya/sebelumnya, sehingga client tidak perlu menyusun query string sendiri:

{
  "data": [ /* ... */ ],
  "links": {
    "self": "/products?page=3&perPage=20",
    "next": "/products?page=4&perPage=20",
    "prev": "/products?page=2&perPage=20"
  }
}

Pendekatan ini sedikit lebih verbose tapi sangat memudahkan client — mereka cukup ikuti link yang diberikan tanpa perlu tahu logic pembentukan URL.

Pagination di GraphQL: Sekilas Konsep Connections

Kalau kamu juga pakai GraphQL selain REST, ada baiknya tahu bahwa GraphQL punya pola standar sendiri bernama Relay Cursor Connections, yang pada dasarnya adalah cursor-based pagination dengan struktur field yang sudah dibakukan (edges, node, pageInfo). Kalau kamu mendesain REST API yang suatu saat mungkin perlu diselaraskan dengan layer GraphQL di atasnya, ada baiknya mempertimbangkan pola cursor-based sejak awal supaya konsepnya lebih mudah dipetakan nanti.

Rekomendasi Praktis

Untuk kebanyakan aplikasi CRUD standar dengan dataset menengah (di bawah beberapa ratus ribu baris) dan UI yang butuh nomor halaman, offset/page-based pagination sudah cukup dan lebih sederhana untuk dikembangkan. Kalau kamu membangun API publik dengan dataset sangat besar, growth tinggi, atau endpoint feed/timeline seperti media sosial, cursor-based adalah pilihan yang jauh lebih aman untuk performa jangka panjang.

Kesalahan Umum Saat Implementasi Pagination

💡 Saat mendesain struktur response pagination, pastikan format JSON-nya tetap konsisten di semua endpoint. Gunakan JSONYAMify untuk memformat dan membandingkan contoh response pagination sebelum difinalisasi di dokumentasi API.
🔧 Rapikan Contoh Response Pagination di JSONYAMify

How to Handle Pagination in JSON API Responses

By Andi Putra Ogie · Updated: July 2026 · 9 min read

Imagine a GET /products endpoint with 500,000 rows of data. Returning all of it at once in a single JSON response would be slow, waste bandwidth, and could exhaust memory on both server and client. Pagination is the standard solution: breaking large data into small "pages" delivered incrementally. This article covers the most common pagination patterns used in modern REST APIs, along with their JSON structure and the pros and cons of each.

1. Offset-Based Pagination (LIMIT/OFFSET)

The simplest and most commonly seen pattern, typically via limit and offset query parameters:

GET /products?limit=20&offset=40
{
  "data": [ /* 20 product items */ ],
  "pagination": {
    "limit": 20,
    "offset": 40,
    "total": 5000
  }
}

Pros: easy to understand and implement, works well for data that rarely changes, allows jumping directly to any page.
Cons: performance degrades sharply at large offsets because the database still has to scan and skip every row before the offset (e.g. OFFSET 100000 still has to pass through 100,000 rows first). Also prone to "shifting data" bugs — if rows are inserted or deleted between the requests for page 1 and page 2, items can be skipped or duplicated.

2. Page-Based Pagination

A variant of offset-based pagination, but using the more human-friendly concept of "page numbers" (e.g. a UI with "page 1, 2, 3..." buttons):

GET /products?page=3&perPage=20
{
  "data": [ /* 20 product items */ ],
  "pagination": {
    "currentPage": 3,
    "perPage": 20,
    "totalPages": 250,
    "totalItems": 5000
  }
}

Technically this is just offset-based pagination wrapped in a more user-friendly form (offset = (page - 1) * perPage). Its pros and cons are the same as offset-based above.

3. Cursor-Based Pagination

Instead of a numeric offset, cursor-based pagination uses a "pointer" (usually an ID or an encoded token) marking the last position already fetched:

GET /products?limit=20&cursor=eyJpZCI6MTIzfQ==
{
  "data": [ /* 20 product items */ ],
  "pagination": {
    "nextCursor": "eyJpZCI6MTQzfQ==",
    "hasMore": true
  }
}

Pros: consistent performance even on large datasets, since the database query can "start right here" without skipping rows (typically implemented as WHERE id > :cursor_id ORDER BY id LIMIT 20). Also not prone to shifting-data bugs like offset-based pagination.
Cons: can't jump directly to a specific page (e.g. "go straight to page 50"), must move sequentially from the previous cursor. Good for infinite scroll, less suited to a page-numbered UI.

This is the pattern used by large APIs like Twitter/X, the Facebook Graph API, and Stripe for list endpoints with large datasets.

4. Keyset Pagination

Similar to cursor-based, but the cursor is explicitly the actual column value (usually a timestamp or ID) rather than an encoded token:

GET /products?limit=20&after_id=143&after_created_at=2026-07-01T10:00:00Z

The underlying database query roughly looks like:

SELECT * FROM products
WHERE (created_at, id) > ('2026-07-01T10:00:00Z', 143)
ORDER BY created_at, id
LIMIT 20

Keyset pagination shares the same performance benefits as cursor-based, but is more transparent since the client can see the actual column values (which is also a downside from a security/abstraction standpoint — some teams prefer hiding it behind an encoded cursor instead).

Quick Comparison

Common Metadata Fields

Regardless of which pattern you pick, these metadata fields commonly show up in pagination responses:

Including Direct Navigation Links (HATEOAS-style)

Some APIs include full URLs for the next/previous page, so clients don't have to build the query string themselves:

{
  "data": [ /* ... */ ],
  "links": {
    "self": "/products?page=3&perPage=20",
    "next": "/products?page=4&perPage=20",
    "prev": "/products?page=2&perPage=20"
  }
}

This approach is slightly more verbose but makes life much easier for clients — they simply follow the given link without needing to know the URL-building logic.

Practical Recommendation

For most standard CRUD applications with a medium-sized dataset (under a few hundred thousand rows) and a UI that needs page numbers, offset/page-based pagination is good enough and simpler to build. If you're building a public API with a very large, fast-growing dataset, or a feed/timeline endpoint like social media, cursor-based pagination is a much safer choice for long-term performance.

Common Mistakes When Implementing Pagination

💡 When designing your pagination response structure, make sure the JSON format stays consistent across every endpoint. Use JSONYAMify to format and compare sample pagination responses before finalizing them in your API documentation.
🔧 Clean Up Your Pagination Response Samples on JSONYAMify