REST API'de bir koleksiyonu daraltmak, sıralamak ve içinde arama yapmak için query parametreleri kullanılır: filtre için alan adını değeriyle yazarsınız (?role=admin&status=active), tarih aralığı için created_after ve created_before gibi sınır parametreleri tanımlarsınız, sıralama için sort parametresine alan adlarını virgülle verir ve azalan sıra için başına eksi işareti koyarsınız (?sort=-created_at,title), serbest metin araması için ise q parametresini kullanırsınız. Sunucu tarafında en önemli kural, sıralanabilir ve süzülebilir alanları bir beyaz listeyle (allowlist) sınırlamaktır.
Bu yazıda bu kalıpların her birini tasarım gerekçesiyle, kenar durumlarıyla ve FastAPI ile çalışan bir uygulama üzerinden ele alıyoruz. Bir değerin neden path yerine query'de durduğu konusu için path ve query parametresi farkı, serinin tamamı için REST API tasarımı öğrenme rehberi sayfasına bakabilirsiniz.
Filtreleme: alan adı eşittir değer

En yalın filtre biçimi, kaynaktaki alanın adını query parametresi olarak kullanmaktır. Aşağıdaki istekler tek filtre, birden çok filtre, tarih aralığı ve arama biçimlerini İngilizce kaynak adlarıyla gösteriyor; aynı kalıplar /kullanicilar?rol=admin gibi Türkçe adlarla da kurulabilir:
GET /users?role=admin HTTP/1.1
Host: api.ornek.com
GET /users?role=admin&status=active HTTP/1.1
Host: api.ornek.com
GET /posts?created_after=2024-01-01&created_before=2024-12-31 HTTP/1.1
Host: api.ornek.com
GET /users?q=john HTTP/1.1
Host: api.ornek.comBirden çok filtre verildiğinde yaygın ve sezgisel anlam "ve" bağlacıdır: role=admin&status=active hem yönetici hem aktif olan kullanıcıları döndürür. Aynı alana birden çok değer vermek ise genellikle "veya" anlamına gelir; ?yazar=ayse&yazar=mehmet iki yazardan birine ait kayıtları ister. Bu iki kuralı açıkça belgelemek gerekir, çünkü istemci geliştiricisi davranışı yalnızca sonuçlara bakarak tahmin etmek zorunda kalmamalıdır. Çoklu değerin tekrar eden parametreyle mi virgülle mi gönderileceği de bir tasarım kararıdır; iki biçimin çatı tarafındaki farkını path ve query parametresi yazısında gösterdik.
Filtre parametresi adlarını kaynak alanlarıyla aynı tutmak istemcinin işini kolaylaştırır: yanıtta status diye bir alan görüyorsa onunla filtreleyebileceğini tahmin eder. Ancak her alanı filtrelenebilir yapmak zorunda değilsiniz. Hangi alanların filtrelenebildiğini belgede listelemek ve listede olmayanlarla gelen istekleri tutarlı biçimde ele almak, hem performansı hem de API sözleşmesini öngörülebilir kılar.
Aralık filtreleri ve tarih biçimi
created_after ve created_before gibi parametreler bir alanın alt ve üst sınırını tanımlar. Burada üç karar vermeniz gerekir. Birincisi biçimdir: tarihleri RFC 3339'daki full-date biçimiyle (2026-03-20) veya saat ve saat dilimi farkı içeren date-time biçimiyle (2026-03-20T09:30:00Z) kabul etmek, istemcinin ve sunucunun aynı dili konuşmasını sağlar. İkincisi sınırların dahil olup olmadığıdır. Adındaki "after" ve "before" sözcükleri sınırın hariç olduğunu çağrıştırır; oysa created_after=2024-01-01&created_before=2024-12-31 gibi bir istek büyük olasılıkla 2024 yılının tamamını istemektedir ve sınırlar hariç tutulursa ilk ve son gün kaybolur. Hangisini seçerseniz seçin, belgeye yazın. Üçüncüsü saat dilimidir: zaman damgası tutan bir alanı yalnızca tarihle süzüyorsanız, o günün hangi saat dilimine göre başlayıp bittiğini tanımlamanız gerekir. Tarih alanlarının yanıtta nasıl yazılacağı için JSON alan adlandırma ve tarih formatı yazısına bakın.
Sıralama: sort parametresi
Sıralama için iki yaygın gösterim vardır. Birincisi, tek bir sort parametresinde alan adlarını virgülle sıralamak ve azalan sıra için başına - koymaktır. İkincisi, alanı sort, yönü ayrı bir order parametresiyle vermektir:
GET /posts?sort=created_at HTTP/1.1
Host: api.ornek.com
GET /posts?sort=-created_at HTTP/1.1
Host: api.ornek.com
GET /posts?sort=-created_at,title HTTP/1.1
Host: api.ornek.com
GET /posts?sort=created_at&order=desc HTTP/1.1
Host: api.ornek.comEksi önekli gösterim birden çok alanla sıralamayı tek parametrede ifade edebildiği için daha esnektir: sort=-created_at,title önce oluşturulma tarihine göre yeniden eskiye, aynı tarihli kayıtları da başlığa göre artan sırada dizer. sort ve order çifti tek alanlı sıralama için daha okunaklıdır ama birden fazla alana genişletmek zordur. Bir API içinde ikisinden yalnızca birini seçin.
Sıralamada gözden kaçan en önemli konu belirleyiciliktir (determinism). Yalnızca created_at ile sıraladığınızda aynı tarihe sahip kayıtların sırası tanımsız kalabilir. Bu, sayfalama ile birleştiğinde bir kaydın iki sayfada birden görünmesine veya hiç görünmemesine yol açabilir. Çözüm, varsayılan sıralamanın sonuna benzersiz bir alan (genellikle id) eklemektir. Aşağıdaki örnekte varsayılan değer bu yüzden -created_at,id olarak seçildi. Sayfalama ayrıntıları için offset sayfalama ve cursor sayfalama yazılarına bakın.
Arama: q parametresi

Serbest metin araması için kısa ve yaygın bir ad q'dur; search da kullanılır. Arama ile filtre arasındaki fark, filtrenin belirli bir alanda tam eşleşme araması, aramanın ise bir veya birden çok metin alanında kısmi eşleşme aramasıdır. ?q=john isteği adında veya e-postasında "john" geçen kullanıcıları döndürebilir; hangi alanlarda arandığını belgede yazmanız gerekir. Arama terimine bir alt sınır (ör. en az iki karakter) ve üst sınır koymak, çok kısa terimlerin tüm tabloyu taramasını ve aşırı uzun girdileri önler.
Türkçe içerikte büyük-küçük harf duyarsız arama ayrıca dikkat ister. Python'un lower() metodu dil bağımsız Unicode kurallarını uygular; bu kurallarda büyük İ harfi tek bir i değil, i ile birleşen bir nokta işaretine dönüşür, büyük I ise noktasız ı yerine noktalı i olur:
print("İstemci".lower() == "istemci")
# çıktı: False
print(len("İ".lower()))
# çıktı: 2
print("ISPARTA".lower())
# çıktı: isparta
def tr_kucuk(metin):
return metin.replace("İ", "i").replace("I", "ı").lower()
print(tr_kucuk("İstemci") == "istemci", tr_kucuk("ISPARTA"))
# çıktı: True ıspartaBu yüzden "İstemci" başlıklı bir kayıt, düz lower() kullanan bir aramada "istemci" terimiyle bulunamaz. Örnekte iki harfi önceden eşleyen küçük bir yardımcı fonksiyon kullandık. Üretimde arama genellikle veritabanında yapıldığı için aynı sorunun karşılığı veritabanının karşılaştırma kuralları (collation) ve metin arama ayarlarıdır; bunları Türkçe veriye göre seçmek gerekir.
FastAPI ile çalışan bir uygulama
Aşağıdaki örnek tüm kalıpları bir /yazilar koleksiyonunda birleştirir: tek değerli durum filtresi, çok değerli yazar filtresi, sınırları dahil tarih aralığı, Türkçe uyumlu arama ve beyaz listeli çoklu sıralama. Veri bellek içi bir listede duruyor; gerçek uygulamada aynı mantık parametreli bir veritabanı sorgusuna çevrilir.
from datetime import date
from fastapi import FastAPI, HTTPException, Query
from fastapi.testclient import TestClient
app = FastAPI()
YAZILAR = [ # bellek içi örnek veri; üretimde veritabanı sorgusu olur
{"id": 1, "baslik": "REST temelleri", "yazar": "ayse", "durum": "yayinda", "created_at": "2026-01-15"},
{"id": 2, "baslik": "Sayfalama rehberi", "yazar": "mehmet", "durum": "taslak", "created_at": "2026-02-03"},
{"id": 3, "baslik": "Hata yanıtları", "yazar": "ayse", "durum": "yayinda", "created_at": "2026-03-20"},
{"id": 4, "baslik": "REST ile sürümleme", "yazar": "zeynep", "durum": "yayinda", "created_at": "2026-03-20"},
{"id": 5, "baslik": "İstemci hataları", "yazar": "mehmet", "durum": "yayinda", "created_at": "2026-04-11"},
]
SIRALANABILIR = {"id", "baslik", "created_at"} # beyaz liste
DURUMLAR = {"taslak", "yayinda"}
def tr_kucuk(metin):
return metin.replace("İ", "i").replace("I", "ı").lower()
def sirala(kayitlar, sort):
alanlar = [a.strip() for a in sort.split(",") if a.strip()]
for alan in alanlar:
ad = alan[1:] if alan.startswith("-") else alan
if ad not in SIRALANABILIR:
raise HTTPException(status_code=400, detail=f"Sıralanamayan alan: {ad}")
for alan in reversed(alanlar): # en önemsiz anahtardan başla, sort kararlıdır
ters = alan.startswith("-")
ad = alan[1:] if ters else alan
kayitlar = sorted(kayitlar, key=lambda k: k[ad], reverse=ters)
return kayitlar
@app.get("/yazilar")
def listele(durum: str | None = None,
yazar: list[str] = Query(default=[]),
created_after: date | None = None,
created_before: date | None = None,
q: str | None = Query(default=None, min_length=2, max_length=100),
sort: str = "-created_at,id"):
if durum is not None and durum not in DURUMLAR:
raise HTTPException(status_code=400, detail=f"Geçersiz durum: {durum}")
if created_after and created_before and created_after > created_before:
raise HTTPException(status_code=400, detail="Tarih aralığı ters verilmiş")
sonuc = YAZILAR
if durum:
sonuc = [y for y in sonuc if y["durum"] == durum]
if yazar:
sonuc = [y for y in sonuc if y["yazar"] in yazar]
if created_after: # sınırlar dahil
sonuc = [y for y in sonuc if date.fromisoformat(y["created_at"]) >= created_after]
if created_before:
sonuc = [y for y in sonuc if date.fromisoformat(y["created_at"]) <= created_before]
if q:
sonuc = [y for y in sonuc if tr_kucuk(q) in tr_kucuk(y["baslik"])]
sonuc = sirala(sonuc, sort)
# Çıktıyı kısa tutmak için yalnızca kimlikleri döndürüyoruz.
return {"data": [y["id"] for y in sonuc], "meta": {"total": len(sonuc)}}
c = TestClient(app)
print(c.get("/yazilar").json())
# çıktı: {'data': [5, 3, 4, 2, 1], 'meta': {'total': 5}}
print(c.get("/yazilar?durum=yayinda&yazar=ayse&yazar=mehmet").json())
# çıktı: {'data': [5, 3, 1], 'meta': {'total': 3}}
print(c.get("/yazilar?created_after=2026-02-01&created_before=2026-03-20").json())
# çıktı: {'data': [3, 4, 2], 'meta': {'total': 3}}
print(c.get("/yazilar?q=rest").json())
# çıktı: {'data': [4, 1], 'meta': {'total': 2}}
print(c.get("/yazilar?q=istemci").json())
# çıktı: {'data': [5], 'meta': {'total': 1}}
print(c.get("/yazilar?sort=baslik").json())
# çıktı: {'data': [3, 4, 1, 2, 5], 'meta': {'total': 5}}
r = c.get("/yazilar?sort=-created_at,sifre_hash")
print(r.status_code, r.json())
# çıktı: 400 {'detail': 'Sıralanamayan alan: sifre_hash'}
r = c.get("/yazilar?durum=silindi")
print(r.status_code, r.json())
# çıktı: 400 {'detail': 'Geçersiz durum: silindi'}
r = c.get("/yazilar?durum=")
print(r.status_code, r.json())
# çıktı: 400 {'detail': 'Geçersiz durum: '}
r = c.get("/yazilar?created_after=2026-05-01&created_before=2026-01-01")
print(r.status_code, r.json())
# çıktı: 400 {'detail': 'Tarih aralığı ters verilmiş'}
r = c.get("/yazilar?created_after=2026-13-01")
print(r.status_code, r.json()["detail"][0]["msg"])
# çıktı: 422 Input should be a valid date or datetime, month value is outside expected range of 1-12
r = c.get("/yazilar?q=a")
print(r.status_code, r.json()["detail"][0]["msg"])
# çıktı: 422 String should have at least 2 charactersÇıktılara sırayla bakalım. Parametresiz istekte varsayılan -created_at,id sıralaması devrede: aynı gün oluşturulan 3 ve 4 numaralı yazılar kimliğe göre kararlı biçimde diziliyor. İkinci istekte durum ve iki değerli yazar filtresi "ve" ile birleşti, taslak durumundaki 2 numaralı yazı elendi. Üçüncü istekte sınırlar dahil olduğu için 20 Mart tarihli iki yazı da sonuçta yer aldı. q=istemci araması, yardımcı fonksiyon sayesinde "İstemci hataları" başlığını buldu.
sort=baslik çıktısı ise bir kenar durumunu ortaya koyuyor: "İstemci hataları" sona düştü. Python metinleri Unicode kod noktası sırasıyla karşılaştırır ve İ harfinin kod noktası tüm ASCII harflerinden büyüktür. Bu, Türk alfabesi sırası değildir; metin alanlarında doğru sıralama da yine veritabanının karşılaştırma kurallarıyla sağlanır.
Hata satırları tasarımın sınırlarını gösteriyor. Beyaz listede olmayan sifre_hash alanıyla sıralama isteği 400 aldı ve hangi alanın sorun çıkardığı yanıtta söylendi. İzin verilmeyen bir durum değeri ve ters verilmiş tarih aralığı da 400 aldı. Ayrıştırılamayan 2026-13-01 tarihi ve iki karakterden kısa arama terimi ise FastAPI'nin otomatik doğrulamasında 422 ile reddedildi. Aynı API içinde hangi durumda 400, hangisinde 422 döneceğinizi tutarlı biçimde belirlemek için 400 ve 422 farkı yazısına bakın.
Boş değerli parametreler
?durum= gibi değeri boş bırakılmış bir parametre, parametrenin hiç gönderilmemesiyle aynı şey değildir. FastAPI bu durumda None yerine boş bir metin verir; örnekteki uygulama boş metni izin verilen değerler arasında bulamadığı için 400 döndürdü. Bazı istemciler formlardaki boş alanları bu şekilde gönderdiği için kararınızı bilinçli vermeniz gerekir: boş değeri hata saymak en açık davranıştır, "filtre yok" anlamına almak ise daha hoşgörülüdür. İkisinden birini seçip belgelemek, istemcinin aynı istekte farklı sonuçlarla karşılaşmasını önler.
Beyaz liste neden şart: getattr tuzağı
Sıralama alanını istemciden alıp doğrudan modelin özelliğine çeviren kod kısa ve çekici görünür. Örneğin getattr(Post, field) ile her alan adı otomatik olarak sıralanabilir hale gelir. Sorun, istemcinin hangi adı gönderebileceğini sınırlamamasıdır:
from fastapi import FastAPI
from fastapi.testclient import TestClient
class Kullanici:
def __init__(self, kid, ad, sifre_hash):
self.id, self.ad, self.sifre_hash = kid, ad, sifre_hash
DB = [Kullanici(1, "Ayşe", "f3a9"), Kullanici(2, "Mehmet", "07c1"), Kullanici(3, "Zeynep", "b6e2")]
app = FastAPI()
@app.get("/kullanicilar")
def listele(sort: str = "id"):
# KÖTÜ: istemcinin verdiği adı doğrudan nesne özelliğine çeviriyor
sirali = sorted(DB, key=lambda k: getattr(k, sort))
return [k.ad for k in sirali]
c = TestClient(app, raise_server_exceptions=False)
print(c.get("/kullanicilar?sort=sifre_hash").json())
# çıktı: ['Mehmet', 'Zeynep', 'Ayşe']
print(c.get("/kullanicilar?sort=yok").status_code)
# çıktı: 500İlk istek parola özetine göre sıralanmış bir liste döndürdü. Özetin kendisi yanıtta görünmese bile, sıralama istemciye gizli bir alanın değerleri hakkında bilgi sızdırır; benzer sorgular tekrarlanarak bu bilgi genişletilebilir. İkinci istekte var olmayan bir alan adı sunucuda yakalanmamış bir hataya ve 500 yanıtına yol açtı; oysa bu bir istemci hatasıdır ve 4xx ile bildirilmesi gerekir. RFC 9110'un güvenlik bölümü de bu noktayı genel olarak işler: istekteki verilere güvenilemez, parametreler sabit değerlerle karşılaştırılmalı ve sonuca göre davranılmalıdır, yorumlayıcıya veya veritabanına doğrudan aktarılmamalıdır. Filtre değerleri için de aynı kural geçerlidir; değerler sorguya her zaman parametreli olarak verilmelidir.
Yanıtta filtre ve sıralamayı yansıtmak
Filtrelenmiş bir koleksiyon yanıtında meta.total değeri tüm tablonun değil, filtreye uyan kayıtların sayısını göstermelidir; aksi halde sayfa sayısı hesabı yanlış çıkar. Uygulanan filtreleri ve sıralamayı yanıtta geri yansıtmak zorunlu değildir ama hata ayıklamayı kolaylaştırır, özellikle sunucunun varsayılan bir sıralama uyguladığı durumlarda:
{
"data": [
{"id": 5, "baslik": "İstemci hataları", "yazar": "mehmet", "durum": "yayinda", "created_at": "2026-04-11"},
{"id": 3, "baslik": "Hata yanıtları", "yazar": "ayse", "durum": "yayinda", "created_at": "2026-03-20"}
],
"meta": {
"total": 4,
"page": 1,
"limit": 2,
"pages": 2,
"filters": {"durum": "yayinda"},
"sort": "-created_at,id"
}
}Sayfalama bağlantıları üretiyorsanız next ve prev adreslerinin aynı filtre ve sıralama parametrelerini taşıması gerekir; yoksa ikinci sayfada istemci farklı bir listeye geçer. Yanıt zarfı tasarımı için JSON yanıt yapısı yazısına bakın.
Önbellek açısından da küçük bir not: RFC 9110'a göre query, hedef kaynağı tanımlayan URI'nin parçasıdır. ?durum=yayinda&yazar=ayse ile ?yazar=ayse&durum=yayinda aynı sonucu döndürse de iki farklı URI'dir. İstemcilerinizin veya kendi bağlantı üreticinizin parametreleri sabit bir sırayla yazması, önbellek isabetini artırır. Önbellek başlıklarının ayrıntısı için Cache-Control ve ETag yazısına bakın.
Sık yapılan hatalar

- Beyaz listesiz sıralama: İstemcinin verdiği alan adını doğrudan özelliğe veya SQL ifadesine çevirmek gizli alanları açığa çıkarır ve beklenmeyen 500 hatalarına yol açar.
- Belirleyici olmayan sıralama: Benzersiz bir son anahtar olmadan sayfalama tekrar eden veya kaybolan kayıtlar üretir.
- Belirsiz aralık sınırları:
created_afterdeğerinin dahil mi hariç mi olduğunu yazmamak, bir günlük veri kayıplarına neden olur. - Saat dilimini tanımlamamak: Yalnızca tarihle yapılan filtrede günün hangi saat dilimine göre hesaplandığı belirsiz kalır.
- Türkçe harfleri hesaba katmamak: Dil bağımsız küçük harfe çevirme "İ" ve "I" harflerinde beklenmeyen sonuç verir.
- Toplamı filtresiz hesaplamak:
meta.totalfiltreye uyan kayıt sayısını göstermelidir. - Her alanı filtrelenebilir yapmak: Belgede listelenmemiş, dizinlenmemiş alanlarla filtreleme hem sözleşmeyi hem performansı belirsizleştirir.
Sık Sorulan Sorular
Filtre için alan adı mı, filter[...] gibi bir sözdizimi mi kullanmalıyım?
Bu yazıdaki kalıp, alan adını doğrudan parametre olarak kullanan en yalın biçimdir ve çoğu ihtiyaç için yeterlidir. Hangi sözdizimini seçerseniz seçin, bütün uç noktalarda (endpoint) aynısını kullanmak ve belgelemek asıl önemli olan kısımdır.
Geçersiz bir filtre değerine 400 mü 422 mi dönmeliyim?
İki yaklaşım da savunulabilir; FastAPI tip uyuşmazlığında kendiliğinden 422 döner, örnekteki elle denetimler ise 400 döndü. Önemli olan, aynı tür hatanın her uç noktada aynı kodu ve aynı hata gövdesini almasıdır. Ayrıntı için istek doğrulama ve hata mesajı formatı yazılarına bakın.
Arama sonuçları nasıl sıralanmalı?
Aramada istemci sort vermediyse sunucunun varsayılan bir sıralama uygulaması ve bunu belgelemesi gerekir. Hangi sıralamanın varsayılan olduğu yanıtta da yansıtılabilir.
Filtre yüzünden URL çok uzarsa ne olur?
RFC 9110 en az 8000 oktetlik URI desteğini önerir ve daha uzun URI'ler için 414 URI Too Long tanımlar. Yüzlerce değer içeren filtreler, istek gövdesiyle çalışan ayrı bir arama uç noktası gerektirebilir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: REST API tasarımı öğrenme rehberi · Path ve query parametresi · Offset sayfalama · Cursor sayfalama · Alan adlandırma ve tarih formatı · 400 ve 422 farkı · REST API güvenliği