# REST API'de sayfalama, filtreleme ve sıralama

> Büyüyen veri kümelerini istemciye yönetilebilir sunmak için API'de sayfalama, filtreleme ve sıralama parametrelerini nasıl tasarladığımı anlatıyorum.

- Yayın: 2016-09-04
- Kategori: Web Geliştirme
- Etiketler: API
- Okuma süresi: 3 dk okuma
- Kaynak: https://www.muhammetsafak.com.tr/blog/rest-api-de-sayfalama-filtreleme-ve-siralama/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
[Bir API ucu](/blog/restful-api-tasariminin-temelleri) yazdığınızda ve ilk denemede tüm kayıtları tek seferde döndürdüğünüzde, küçük ölçekte bu sorun yaratmaz. Ama tablo büyüdüğünde ve istemci 10.000 kayıt almaya başladığında hem sunucu hem de istemci yavaşlar. Bunun yanı sıra istemci genellikle tümünü değil, belirli bir kısmını — belirli koşullara uyan, belirli bir sıraya göre düzenlenmiş kayıtları — istiyor.

Sayfalama, filtreleme ve sıralama bu sorunu çözen üç ayrı ama birlikte düşünülmesi gereken kavramdır. Bu yazıda bunları API düzeyinde nasıl kurduğumu anlatıyorum.

## Sayfalama

En yaygın sayfalama yöntemi, `page` ve `per_page` parametrelerini kullanmak:

```
GET /api/urunler?page=2&per_page=20
```

[Laravel'de](/blog/laravel-ile-json-api-uclari-yazmak) `paginate()` metodu bunu zaten destekliyor; sorguya yalnızca `per_page` değerini geçirmek yeterli:

```php
<?php

namespace App\Http\Controllers\Api;

use App\Urun;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class UrunController extends Controller
{
    public function index(Request $request)
    {
        $adet = $request->input('per_page', 20);

        // Maksimum sınır koyuyorum — istemcinin 1000 kayıt istemesini engelliyorum
        $adet = min($adet, 100);

        $urunler = Urun::paginate($adet);

        return response()->json($urunler);
    }
}
```

`paginate()` yanıta `total`, `per_page`, `current_page`, `last_page` gibi meta bilgileri de ekliyor; istemci kaç sayfa olduğunu ve şu an hangi sayfada olduğunu bu bilgilerden anlıyor.

## Filtreleme

Filtreleme için URL parametrelerini kullanıyorum:

```
GET /api/urunler?kategori=elektronik&min_fiyat=100&max_fiyat=500
```

Bu parametreleri sorguya yansıtmak için koşullu bloklar yazıyorum:

```php
public function index(Request $request)
{
    $sorgu = Urun::query();

    if ($request->filled('kategori')) {
        $sorgu->where('kategori', $request->input('kategori'));
    }

    if ($request->filled('min_fiyat')) {
        $sorgu->where('fiyat', '>=', (float) $request->input('min_fiyat'));
    }

    if ($request->filled('max_fiyat')) {
        $sorgu->where('fiyat', '<=', (float) $request->input('max_fiyat'));
    }

    if ($request->filled('arama')) {
        $aranan = '%' . $request->input('arama') . '%';
        $sorgu->where('ad', 'like', $aranan);
    }

    $adet    = min($request->input('per_page', 20), 100);
    $urunler = $sorgu->paginate($adet);

    return response()->json($urunler);
}
```

`filled()` metodu hem parametre varlığını hem de boş olmadığını kontrol ediyor; boş string gönderildiğinde filtre uygulanmıyor.

## Sıralama

Sıralama için `sort` ve `order` parametreleri kullanıyorum:

```
GET /api/urunler?sort=fiyat&order=asc
GET /api/urunler?sort=olusturulma_tarihi&order=desc
```

Güvenli bir yaklaşım için izin verilen alanları beyaz listeyle kontrol ediyorum; istemcinin herhangi bir alana göre sıralama yapmasına izin vermiyorum:

```php
public function index(Request $request)
{
    $sorgu = Urun::query();

    // Filtreleme (yukarıdaki gibi)...

    // İzin verilen sıralama alanları
    $izinliAlanlar = ['ad', 'fiyat', 'created_at'];
    $siralama      = $request->input('sort', 'created_at');
    $yon           = $request->input('order', 'desc');

    if (in_array($siralama, $izinliAlanlar)) {
        $sorgu->orderBy($siralama, $yon === 'asc' ? 'asc' : 'desc');
    }

    $adet    = min($request->input('per_page', 20), 100);
    $urunler = $sorgu->paginate($adet);

    return response()->json($urunler);
}
```

`in_array` kontrolü olmadan istemci `sort=password` gibi hassas alanlara sıralama yapabilir ya da ham SQL enjeksiyonu denemesi olabilir. Beyaz liste bu riski ortadan kaldırıyor.

## Yanıt yapısı

Sayfalama meta bilgisini gövdede ayrı tutmak, istemcinin yanıtı kolayca işlemesini sağlıyor. Laravel'in `paginate()` çıktısı şuna benziyor:

```json
{
    "current_page": 2,
    "data": [
        { "id": 21, "ad": "Klavye", "fiyat": 249.90 },
        { "id": 22, "ad": "Mouse", "fiyat": 189.50 }
    ],
    "from": 21,
    "last_page": 15,
    "per_page": 20,
    "to": 40,
    "total": 287
}
```

İstemci `total` ve `per_page` değerlerinden kaç sayfa olduğunu hesaplayabiliyor, `current_page` ile nerede olduğunu biliyor.

## Birleşik örnek

Gerçek bir çağrı şöyle görünüyor:

```
GET /api/urunler?kategori=elektronik&min_fiyat=100&sort=fiyat&order=asc&page=1&per_page=15
```

Bu çağrı: elektronik kategorisinden, 100 TL üzeri ürünleri, fiyata göre artan sırayla, ilk sayfada 15 adet döndürüyor.

## Küçük ama önemli bir ayrıntı: parametre adı tutarlılığı

Bu üç özelliği tasarlarken en çok dikkat ettiğim şey, parametre adlarının başından tutarlı olmasıydı. Bir uca `page`, diğerine `pageNumber`, başka birine `sayfa` demek istemci geliştiricisini gereksiz yere yoruyor. API yüzeyinin tamamında aynı adları kullanmak, belgelemeyi azaltıyor ve hataları önlüyor.

Şu an için küçük bir proje de olsa, bu alışkanlığı başından kurmak sonraki uçları eklerken otomatik bir tutarlılık sağlıyor.

## Kursor tabanlı sayfalama hakkında

`page` ve `per_page` ile sayfalama, büyük tablolarda bir sorun yaratıyor: `OFFSET` sorgusu. On bininci sayfaya gitmek için veritabanının 200.000 kaydı okuyup geçmesi gerekiyor; bu tablo büyüdükçe yavaşlıyor.

Şu an bu sorunu yaşamıyorum çünkü tablolarım henüz o ölçeğe ulaşmadı. Ama cursor pagination denen bir alternatif var: "son gördüğüm kaydın ID'sini ver, ondan sonrasını getir" mantığıyla çalışıyor. Laravel'de `cursorPaginate()` metodu bu yöntemi uyguluyor. Ölçek sorunu olmadığında `paginate()` yeterli; gerektiğinde geçiş yapmak nispeten kolay.

---

Bu üç özelliği başından doğru kurmak, sonraki geliştirmeleri kolaylaştırıyor. İstemci geliştiricisi sayfalamayı kendi başına çözmek zorunda kalmıyor; filtreleme için ayrı uç noktalar açmak gerekmediğinden API yüzeyi küçük kalıyor. Mobil istemci veya üçüncü parti bir entegrasyon geldiğinde, aynı parametrelerle aynı uç noktayı kullanıyor. Bu tutarlılık başta görünmez ama ilerledikçe değerini hissettiriyor; yeni bir istemci için ayrı bir uç yazmak ya da farklı sayfalama mantığı uydurmak gerekmediğinde kazanılan zaman birikiyor.
