Episode 12 membahas pagination di GraphQL: mengapa pagination penting, offset-based pagination dengan limit dan skip, cursor-based pagination dengan stable cursor, Relay Connection Specification dengan edge node dan pageInfo, hingga best practices ukuran halaman dan caching.

Mengembalikan seluruh daftar data dalam satu response adalah resep bencana. Episode 12 membahas pagination — teknik mengambil data dalam potongan-potongan terkontrol — yang wajib ada pada hampir semua field list di API production.
Kita akan membandingkan offset-based dan cursor-based pagination, membedah Relay Connection Specification yang menjadi standar de facto, lalu mempelajari implementasi dan best practices-nya, termasuk ukuran halaman default dan batas maksimum.
Tanpa pagination, daftar jutaan item dikirim sekaligus: response raksasa, database terbebani, dan aplikasi mobile membuang bandwidth. Pagination menyelesaikan tiga masalah:
Aturan praktis industri: semua field list wajib memiliki pagination, kecuali list yang dijamin kecil dan berbatas.
Pola paling sederhana: limit menentukan ukuran halaman, skip menentukan berapa item yang dilewati.
type Query {
posts(limit: Int = 20, skip: Int = 0): [Post!]!
}Query: {
posts: (_, args, ctx) =>
ctx.db.posts.findMany({ take: args.limit, skip: args.skip }),
},Keunggulannya sederhana dan intuitif, serta mudah mencari total item. Kelemahannya muncul saat data berubah: ketika item baru disisipkan di tengah, halaman berikutnya akan bergeser — item terlewat atau terduplikasi. Untuk feed dinamis, pola ini kurang cocok.
Cursor adalah penanda posisi yang stabil dalam kumpulan data. Alih-alih menghitung "lewati 20 item", server menerima "mulai dari setelah item ini":
type Query {
posts(after: String, first: Int = 20): PostConnection!
}Query: {
posts: async (_, args, ctx) => {
const decoded = args.after ? Buffer.from(args.after, "base64").toString() : null;
const cursor = decoded ? { id: Number(decoded) } : undefined;
const posts = await ctx.db.posts.findMany({
take: args.first + 1,
...(cursor && { cursor, skip: 1 }),
});
return {
edges: posts.slice(0, args.first).map((p) => ({ node: p, cursor: encodeCursor(p.id) })),
pageInfo: { hasNextPage: posts.length > args.first },
};
},
},Perhatikan pola penting: ambil first + 1 item untuk menentukan hasNextPage. Cursor di-encode sebagai base64 dari id unik — ini membuat cursor stable: walau ada item baru masuk, halaman berikutnya tetap mulai dari posisi yang benar. Itulah keunggulan utama cursor dibanding offset.
Relay Connection Specification adalah standar dari Relay (episode 26) yang kini dipakai luas. Strukturnya terdiri dari:
connection dengan field edges dan pageInfo.edge yang membungkus node dan cursor masing-masing item.pageInfo dengan hasNextPage, hasPreviousPage, serta startCursor dan endCursor.type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
posts(first: Int = 20, after: String): PostConnection!
postsBackward(last: Int = 20, before: String): PostConnection!
}Mengapa Relay spec penting? Karena client framework memahami struktur ini secara universal: infinite scroll, tombol previous, dan refresh daftar bisa dibangun tanpa mengenal detail schema masing-masing server. Cursor juga mendukung navigasi maju (after) dan mundur (before).
Untuk menghitung total item (misalnya untuk badge jumlah), jalankan query count seperti SELECT count(*) FROM posts terpisah atau simpan sebagai field di connection. Ingat bahwa menambahkan count bisa mahal pada tabel besar — pertimbangkan caching atau estimasi.
page dan pageSize.const MAX_LIMIT = 100;
const limit = Math.min(args.first ?? 20, MAX_LIMIT);Hasil paginated bersifat cacheable bila urutan datanya stabil. Di client, simpan endCursor dari response terakhir untuk request berikutnya, dan pertahankan item yang sudah dimuat saat memuat halaman baru. Ini menjadi dasar infinite scroll yang halus — detail sisi client-nya akan dibahas di episode 25.
Inti yang harus dibawa pulang:
first + 1 item untuk menentukan hasNextPage.Di episode 13 selanjutnya kalian akan mempelajari authentication dengan JWT — konsep authentication versus authorization, struktur JWT, implementasi mutation login dan signup dengan bcrypt, context-based authentication, pola refresh token, hingga integrasi OAuth Google dan GitHub. Keamanan akses ke API kalian akan dimulai!