Shopify Admin API’de ?page=2 diye bir şey yok. Sayfa numarası yıllar önce kaldırıldı, yerine cursor geldi.
GraphQL tarafında ise limit istek sayısı değil, sorgu maliyeti. İkisini de bilmeden on bin ürünlük katalog çekilmiyor.
REST: Link başlığı
REST uç noktalarında sonraki sayfa cevabın gövdesinde değil, Link başlığında geliyor.
Link: <https://shop.myshopify.com/admin/api/2025-07/products.json?limit=250&page_info=eyJsYXN0...>; rel="next"
Bu URL’yi parse edip page_info değerini almanız gerekiyor. Kendiniz üretemezsiniz, imzalı bir değer.
Bir tuzak var: page_info ile birlikte limit dışında filtre gönderemezsiniz. İlk istekte verdiğiniz filtreler cursor’a gömülü. status=active filtresini ikinci sayfada tekrar göndermeye çalışırsanız hata alırsınız.
$url = "https://{$shop}/admin/api/2025-07/products.json?limit=250&status=active";
while ($url) {
$response = Http::withHeaders([...])->get($url);
$this->process($response->json('products'));
$url = $this->nextLink($response->header('Link')); // yoksa null
}
GraphQL: pageInfo
GraphQL tarafı daha temiz. Cursor cevabın içinde geliyor.
query ($cursor: String) {
products(first: 250, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
title
totalInventory
}
}
}
hasNextPage false olana kadar endCursor değerini after olarak geri gönderirsiniz. Link başlığı parse etmek yok.
Maliyet, istek değil
GraphQL Admin API’de saniyede kaç istek attığınız önemli değil. Her sorgunun bir puan maliyeti var ve bir puan havuzundan düşüyor.
Standart planda havuz 1000 puan, saniyede 50 puan geri doluyor. Yani sürdürülebilir hızınız saniyede 50 puan, ani yükte 1000 puanlık pencereniz var.
Maliyeti tahmin etmeyin, cevaptan okuyun. Her yanıtın extensions alanında geliyor:
{
"extensions": {
"cost": {
"requestedQueryCost": 302,
"actualQueryCost": 52,
"throttleStatus": {
"maximumAvailable": 1000,
"currentlyAvailable": 948,
"restoreRate": 50
}
}
}
}
currentlyAvailable değeri düştükçe yavaşlayın. Sıfıra inip 429 yemeyi beklemeyin.
$status = $response->json('extensions.cost.throttleStatus');
if ($status['currentlyAvailable'] < 200) {
usleep(500_000);
}
İstenen maliyet ile gerçek maliyet
Yukarıdaki örnekte requestedQueryCost 302, actualQueryCost 52. Aradaki fark önemli.
Shopify sorguyu çalıştırmadan önce en kötü durum maliyetini hesaplıyor. first: 250 yazdıysanız 250 kayıt gelecek varsayıyor. Gerçekte 40 kayıt döndüyse gerçek maliyet düşük oluyor ve fark size iade ediliyor.
Ama havuz kontrolü istenen maliyet üzerinden yapılıyor. first: 250 ile iç içe üç bağlantı çekerseniz, sorgu tek satır bile dönmese havuzu boşaltabilirsiniz.
Pratik sonuç: iç içe bağlantılarda first değerini küçük tutun. Ürün başına 250 varyant istemeyin, 10 isteyin.
Bulk operations
On bin ürünü baştan sona çekiyorsanız sayfalama yanlış araç. bulkOperationRunQuery kullanın.
Sorguyu Shopify’a verirsiniz, arka planda çalıştırır, sonucu JSONL dosyası olarak sunar. Puan maliyeti sabit ve düşük.
Karşılığı gecikme. İş bitene kadar beklemeniz gerekiyor, anlık değil. İlk dolum ve gecelik tam senkron için doğru araç; sipariş ekranında canlı veri için değil.
Özet
REST’te Link başlığını parse et, filtreleri ikinci sayfada tekrar gönderme. GraphQL’de pageInfo kullan ve throttleStatus değerine bakarak yavaşla. Tam katalog için bulk operation.
Bunları bilmeden yazılan senkron kodu ilk gün çalışır, katalog büyüyünce durur.