REST API tasarımında en sık yapılan hatalar şunlardır: URL'ye fiil koymak (/getUsers), kaynak adını tekil yazmak (/user), adresleri gereğinden derin kurmak, hata durumunda 200 döndürmek, her uç noktada farklı bir hata gövdesi kullanmak, koleksiyonları sayfalamadan döndürmek, CORS ayarında allow_origins=["*"] bırakmak ve yanıtlarda parola özeti, anahtar ya da hata izi gibi gizli bilgileri sızdırmak. Her biri ilk gün küçük görünür, fakat API kullanılmaya başladıktan sonra düzeltmek sürüm değişikliği gerektirebilir.

Bu yazı her hatayı yanlış ve doğru örnek çiftiyle, nedenini ve etkisini açıklayarak anlatıyor; bazılarının etkisini FastAPI ile çalıştırılmış örneklerle gösteriyor. Aynı konuların "yapılması gerekenler" yönü REST API tasarım kontrol listesinde, serinin tamamı REST API tasarımı öğrenme rehberi sayfasında.

1. URL'ye fiil koymak

1. URL'ye fiil koymak: GET /getUsers, GET /getUser?id=123, POST /createUser, POST /updateUser

Eylem tabanlı URL'ler (/getUsers, /createUser, /deleteUser) API'yi bir uzak yordam çağrısı listesine dönüştürür. Sorun estetik değil: HTTP metodu zaten eylemi taşır, fiili URL'ye de yazınca aynı bilgi iki kez söylenir ve ikisi çeliştiğinde (GET /deleteUser?id=5) hangisinin geçerli olduğu belirsizleşir. Daha kötüsü, GET güvenli (safe) bir metottur: RFC 9110 bu ayrımın amacını, otomatik tarayıcıların (spider) ve önbellek ön yüklemesinin (pre-fetching) zarar verme kaygısı olmadan çalışabilmesi olarak açıklar. Veri silen bir GET bu araçların kendiliğinden tetikleyebileceği bir işlem hâline gelir; RFC de URI'deki parametrelerle eylem seçilen kaynaklarda eylemin metodun anlamıyla tutarlı olmasının kaynak sahibinin sorumluluğu olduğunu vurgular. Her yeni eylem için yeni bir adres icat edildiğinden istemci de adresleri tahmin edemez.

Eylem tabanlı (yanlış)Kaynak tabanlı (doğru)
GET /getUsersGET /users
GET /getUser?id=123GET /users/123
POST /createUserPOST /users
POST /updateUserPUT /users/123 ya da PATCH /users/123
POST /deleteUserDELETE /users/123

Kaynak tabanlı tabloda yalnızca iki adres var: koleksiyon ve tekil öğe. Bütün işlemler bu iki adres üzerinde metotla ayrılıyor. Ayrıntılı kurallar endpoint isimlendirme ve CRUD endpoint tasarımı yazılarında; REST'in kaynak fikri REST API nedir yazısında.

2. Tekil kaynak adı ve karışık adlandırma

/user/123 ile /users aynı API'de birlikte bulunduğunda istemci hangi kaynağın hangi biçimde yazıldığını ezberlemek zorunda kalır. Bu serinin dayandığı API tasarımı rehberi kaynak adlarını çoğul yazmayı önerir; gerekçesi tutarlılıktır: koleksiyon /users, içindeki öğe /users/123, ikisi aynı kökü paylaşır. Bu bir RFC kuralı değil, bir tasarım tercihidir; bir API'nin tamamen tekil adlar kullanması da tutarlı olabilir. Asıl hata karışıklıktır. Aynı kategori hatanın başka biçimleri de var: bir uç noktada /order-items, diğerinde /orderItems yazmak; bir yerde Türkçe /siparisler, başka yerde İngilizce /users kullanmak. JSON alan adlarında da aynı tutarlılık gerekir (alan adlandırma).

3. Gereğinden derin URL

İlişkileri adreste göstermek doğaldır: /users/123/posts "123 numaralı kullanıcının yazıları" demektir. Ancak zincir uzadıkça adres kırılganlaşır. /users/123/posts/456/comments/789/replies adresini kullanabilmek için istemcinin yorumun hangi yazıya, yazının hangi kullanıcıya ait olduğunu bilmesi gerekir; oysa yorumun kimliği zaten tektir. Bir yazı başka bir kullanıcıya taşınırsa bütün alt adresler değişir. Rehber en fazla üç seviye önerir ve bu örnek için /comments/789/replies yolunu gösterir.

http
GET /users/123/posts/456/comments/789/replies HTTP/1.1
Host: api.ornek.com.tr

GET /comments/789/replies HTTP/1.1
Host: api.ornek.com.tr

İkinci istek aynı veriyi istiyor, ama yalnızca gerçekten gereken kimliği taşıyor. Hangi ilişkinin yola, hangisinin sorgu parametresine konacağı iç içe kaynaklar ve URL derinliği yazısında tartışılıyor.

4. Hata için 200 döndürmek

Bazı API'ler her yanıtı 200 ile gönderir ve başarıyı gövdedeki bir alanla bildirir. Bu tasarım HTTP'nin en temel sözleşmesini bozar: durum kodu, yanıtın ne anlama geldiğini gövdeyi okumadan söyleyen tek ortak dildir. İstemci kütüphaneleri, yeniden deneme mantıkları, izleme araçları ve ağ geçitleri hep bu koda bakar.

http
GET /v1/siparisler/9 HTTP/1.1
Host: api.ornek.com.tr

HTTP/1.1 200 OK
Content-Type: application/json

{"success": false, "error": "Sipariş bulunamadı"}
http
GET /v1/siparisler/9 HTTP/1.1
Host: api.ornek.com.tr

HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{"type": "https://api.ornek.com.tr/hatalar/bulunamadi", "title": "Kaynak bulunamadı", "status": 404, "detail": "9 numaralı sipariş yok"}

Etkisini somut görmek için aşağıdaki örnekte iki uç nokta aynı durumu farklı biçimde bildiriyor ve istemci tarafında httpx'in raise_for_status() yöntemi çağrılıyor. Bu yöntem 4xx ve 5xx kodlarında hata fırlatır; birçok istemci kodu hatayı tam olarak bu yolla yakalar. Aynı örnek bir sonraki bölümde kullanacağımız sayfalı liste uç noktasını da içeriyor.

python
import httpx
from fastapi import FastAPI, HTTPException, Query
from fastapi.testclient import TestClient

app = FastAPI()
SIPARISLER = [{"id": i, "tutar": i * 10} for i in range(1, 501)]

@app.get("/v1/hatali/siparisler/{no}")
def hatali(no: int):
    return {"success": False, "error": "Sipariş bulunamadı"}      # durum kodu 200

@app.get("/v1/siparisler/{no}")
def dogru(no: int):
    raise HTTPException(status_code=404, detail="Sipariş bulunamadı")

@app.get("/v1/siparisler")
def listele(page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100)):
    bas = (page - 1) * limit
    return {"data": SIPARISLER[bas:bas + limit],
            "meta": {"total": len(SIPARISLER), "page": page, "limit": limit}}

c = TestClient(app)
for yol in ["/v1/hatali/siparisler/9", "/v1/siparisler/9"]:
    r = c.get(yol)
    try:
        r.raise_for_status()
        print(yol, r.status_code, "hata yakalanmadı:", r.json())
    except httpx.HTTPStatusError:
        print(yol, r.status_code, "hata yakalandı")

r = c.get("/v1/siparisler")
print(len(r.json()["data"]), r.json()["meta"])
r = c.get("/v1/siparisler?limit=100000")
print(r.status_code, r.json()["detail"][0]["msg"])

# çıktı:
# /v1/hatali/siparisler/9 200 hata yakalanmadı: {'success': False, 'error': 'Sipariş bulunamadı'}
# /v1/siparisler/9 404 hata yakalandı
# 20 {'total': 500, 'page': 1, 'limit': 20}
# 422 Input should be less than or equal to 100

İlk satırda istemci "sipariş bulunamadı" bilgisini almış olmasına rağmen hatayı yakalayamadı; kod başarılı bir yanıt gibi devam etti. Uygulamanın gerçek kullanımda bu gövdeyi bir sipariş nesnesi gibi işlemeye çalıştığını düşünün. İkinci satırda 404 doğru biçimde hataya dönüştü. Hangi durum için hangi kodun seçileceği durum kodu seçimi yazısında senaryo senaryo anlatılıyor.

5. Uç noktadan uç noktaya değişen hata gövdesi

5. Uç noktadan uç noktaya değişen hata gövdesi

Farklı geliştiricilerin farklı zamanlarda yazdığı uç noktalarda hata gövdeleri birbirinden ayrışmaya meyillidir. Aşağıdaki dizi aynı API'nin üç uç noktasından dönen üç doğrulama hatasını gösteriyor:

json
[{"error": "email geçersiz"}, {"message": "Validation failed", "fields": {"email": "invalid"}}, {"detail": [{"loc": ["body", "email"], "msg": "geçersiz"}]}]

İstemci bu üç yapıyı ayrı ayrı tanımak zorunda; yeni bir uç nokta eklendiğinde dördüncü bir yapı gelmeyeceğinin de güvencesi yok. Çözüm, tek bir biçim seçip her yerde uygulamaktır. Standart bir seçenek RFC 9457 Problem Details'tır (rehber metni bu biçimi eski numarası RFC 7807 ile anar; RFC 9457 onun yerini almıştır). Alternatif olarak rehberde gösterilen, makine okunur bir code alanı içeren kendi biçiminizi kullanabilirsiniz. FastAPI kullanıyorsanız çatının kendi 422 yanıtının da (üçüncü öğedeki detail listesi) bu biçimden farklı olduğunu unutmayın; bir istisna işleyicisiyle onu da ortak biçime çevirmeniz gerekir. Ayrıntılar: Problem Details, hata mesajı formatı ve hata kodları.

6. Sayfalamasız liste

GET /v1/siparisler bugün elli kayıt döndürüyor olabilir. Sipariş sayısı büyüdükçe aynı istek önce yavaşlar, sonra sunucunun belleğini zorlar, sonunda zaman aşımına düşer. Sayfalamayı sonradan eklemek ise yanıt yapısını düz bir diziden data ve meta zarfına çevirdiği için mevcut istemcileri bozar. Yukarıdaki örnekteki listele uç noktası doğru yaklaşımı gösteriyor: varsayılan limit 20, üst sınır 100; 500 kayıtlık listede ilk istek yalnızca 20 kayıt döndürdü ve meta toplam sayıyı verdi. limit=100000 isteği ise 422 ile reddedildi. Üst sınırı olmayan bir limit parametresi sayfalama yokmuş gibi davranır; istemci tek istekte bütün tabloyu isteyebilir. Ayrıntılar: offset sayfalama, cursor pagination.

7. allow_origins=["*"] ile yayına çıkmak

Rehberdeki CORS örneği allow_origins=["*"] satırını yorum olarak gösterir ve yanına "yalnızca geliştirme" notunu düşer. Bu not çoğu zaman unutulur ve ayar üretime taşınır. Daha sinsi bir sorun, joker kökenin allow_credentials=True ile birleşmesidir. Aşağıdaki deneme Starlette'in (FastAPI'nin altındaki çatı) bu birleşimde ne yaptığını gösteriyor:

python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
                   allow_methods=["*"], allow_headers=["*"])

@app.get("/v1/profil")
def profil():
    return {"ad": "Ali Can"}

c = TestClient(app)
r = c.get("/v1/profil", headers={"Origin": "https://kotu-site.example", "Cookie": "oturum=abc"})
print(r.headers.get("access-control-allow-origin"), r.headers.get("access-control-allow-credentials"))

# çıktı:
# https://kotu-site.example true

Yanıt * değil, isteği gönderen sitenin kökenini aynen geri yazdı ve kimlik bilgisine izin verdi. Yani bu sürümde "her köken" ayarı, çerezle yapılan isteklerde de her siteye kapıyı açan bir yansıtmaya dönüştü. Doğrusu, izin verilen kökenleri tek tek yazmaktır: allow_origins=["https://panel.ornek.com.tr"]. API yalnızca sunucudan sunucuya çağrılıyorsa CORS ara katmanına hiç gerek yoktur. CORS'un nasıl çalıştığı ve başlıkların anlamı CORS nedir, tarayıcıda görülen hata mesajları CORS hataları ve çözümleri yazısında.

8. Yanıtta gizli bilgi

8. Yanıtta gizli bilgi

Rehberin kontrol listesinin son maddesi "yanıtlarda sır yok" der. En sık rastlanan sızıntı biçimi, veritabanı kaydını olduğu gibi döndürmektir: kayıtta parola özeti, API anahtarı ya da iç notlar varsa bunlar da istemciye gider. Aşağıdaki örnekte aynı kayıt iki uç noktadan dönüyor; birinde yanıt şeması tanımlı değil, diğerinde response_model ile tanımlı.

python
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel

app = FastAPI()
DB = {7: {"id": 7, "email": "ali@ornek.com.tr", "ad": "Ali Can",
          "sifre_ozeti": "$2b$12$ornekozet", "api_anahtari": "sk_live_ornek"}}

class KullaniciCikti(BaseModel):
    id: int
    email: str
    ad: str

@app.get("/v1/hatali/kullanicilar/{kullanici_id}")
def hatali(kullanici_id: int):
    return DB[kullanici_id]                 # kayıt olduğu gibi döner

@app.get("/v1/kullanicilar/{kullanici_id}", response_model=KullaniciCikti)
def dogru(kullanici_id: int):
    return DB[kullanici_id]                 # response_model fazlasını atar

c = TestClient(app)
print(c.get("/v1/hatali/kullanicilar/7").json())
print(c.get("/v1/kullanicilar/7").json())

# çıktı:
# {'id': 7, 'email': 'ali@ornek.com.tr', 'ad': 'Ali Can', 'sifre_ozeti': '$2b$12$ornekozet', 'api_anahtari': 'sk_live_ornek'}
# {'id': 7, 'email': 'ali@ornek.com.tr', 'ad': 'Ali Can'}

response_model yalnızca modelde tanımlı alanları geçiriyor, gerisini atıyor. Bu yaklaşımın ikinci yararı da var: veritabanına yeni bir sütun eklendiğinde yanıt kendiliğinden genişlemez, alanı yanıta eklemek bilinçli bir karar olur. Sızıntının ikinci biçimi hata ayrıntısıdır. Aşağıdaki örnekte bir uç nokta, içinde bağlantı dizesi geçen bir istisna fırlatıyor; uygulama bir kez hata ayıklama kipinde, bir kez normal kipte çalıştırılıyor:

python
from fastapi import FastAPI
from fastapi.testclient import TestClient

def uygulama(debug: bool) -> FastAPI:
    app = FastAPI(debug=debug)

    @app.get("/v1/rapor")
    def rapor():
        baglanti = "postgresql://rapor:Gizli123@10.0.0.5/uretim"
        raise RuntimeError(f"bağlantı kurulamadı: {baglanti}")

    return app

for debug in (True, False):
    r = TestClient(uygulama(debug), raise_server_exceptions=False).get("/v1/rapor")
    print(debug, r.status_code, r.headers["content-type"], "Gizli123" in r.text)

# çıktı:
# True 500 text/plain; charset=utf-8 True
# False 500 text/plain; charset=utf-8 False

debug=True iken 500 yanıtının gövdesi, istisna mesajındaki parolayı da içeren hata izini taşıdı; normal kipte gövde genel bir mesajla sınırlı kaldı. Hata ayıklama kipi geliştirme ortamına aittir. Üretimde ayrıntılar sunucu günlüğüne yazılır, istemciye yalnızca genel bir mesaj ve gerekirse günlükte aramayı kolaylaştıracak bir hata kimliği gider. API güvenliğinin geri kalanı REST API güvenlik rehberinde.

Kimlik hatalarında yanlış kod

Sekiz hatanın dışında, durum kodu hatalarının özel bir türü ayrıca anılmayı hak ediyor: 401 ile 403'ü karıştırmak. RFC 9110'a göre 401 Unauthorized, isteğin hedef kaynak için geçerli kimlik bilgisi taşımadığını anlatır ve 401 üreten sunucu en az bir kimlik doğrulama yöntemi bildiren WWW-Authenticate başlığını göndermek zorundadır (MUST). 403 Forbidden ise sunucunun isteği anladığını ama yerine getirmeyi reddettiğini söyler; kimlik bilgisi gönderilmişse bunlar erişim için yetersiz sayılmıştır ve istemci aynı bilgilerle isteği kendiliğinden tekrarlamamalıdır. Süresi dolmuş bir token için 403, yetkisi olmayan bir kullanıcı için 401 döndürmek istemcinin yanlış tepki vermesine yol açar: ilkinde yeniden giriş yapması gerekirken yapmaz, ikincisinde boşuna yeniden giriş dener. Başlığı unutulmuş 401 de standarda aykırıdır. Ayrıntılar: 401 ve 403 farkı, JWT Bearer kimlik doğrulama.

Örnek kodları olduğu gibi kopyalamak

Son bir hata türü tasarımdan çok uygulamayla ilgili, ama API'yi doğrudan etkiliyor: belgelerden alınan örnek kodları sürüm farklarını kontrol etmeden kullanmak. Bu serinin dayandığı başvuru rehberindeki kodlarda da birkaç örnek var. Pydantic 2'de .dict() yerine model_dump() kullanılır. Python 3.12'de datetime.utcnow() kullanımdan kaldırılmıştır ve saat dilimi bilgisi taşımayan bir zaman üretir; yerine datetime.now(timezone.utc) yazılır. EmailStr türü ek bir paket gerektirir; paket kurulu değilse uygulama açılışta hata verir. Sıralama örneğindeki getattr(Post, field) kullanımı istemcinin gönderdiği her alan adını kabul eder; bu yüzden sıralanabilir alanlar bir izin listesiyle sınırlanmalıdır (filtreleme ve sıralama). Webhook gönderim örneğindeki zaman aşımı ve hata işleme eksikleri webhook tasarımı yazısında ayrıca ele alınıyor.

Hataların özeti

HataBelirtisiDüzeltme
Fiilli URL/getUsers, /createUserKaynak adı ve HTTP metodu
Tekil ya da karışık ad/user ile /users birlikteTek bir kural, tercihen çoğul
Derin URLDört ve daha fazla seviye/comments/789/replies gibi kısa yol
Hata için 200{"success": false} gövdesi4xx ve 5xx kodları
Tutarsız hata gövdesiHer uç noktada başka alanlarProblem Details ya da tek özel biçim
Sayfalamasız listeTüm tabloyu döndüren GETlimit, varsayılan ve üst sınır
Joker CORSallow_origins=["*"] üretimdeİzinli kökenleri tek tek yazmak
Yanıtta sırParola özeti, anahtar, hata iziYanıt şeması ve kapalı hata ayıklama

Sık Sorulan Sorular

Mevcut bir API'de bu hataları nasıl düzeltirim?

URL ve yanıt yapısını değiştiren düzeltmeler mevcut istemcileri bozar. Bu yüzden yeni yapıyı yeni bir sürümde (/v2) sunup eski sürümü bir takvimle emekliye ayırmak en güvenli yoldur (geriye uyumluluk, Deprecation ve Sunset). Yanıttan gizli alanı çıkarmak ise beklemeden yapılmalıdır.

Hata için 200 döndürmek hiçbir zaman doğru değil mi?

İstek gerçekten başarısız olduysa doğru değildir. Ancak toplu işlemlerde isteğin kendisi işlenmiş, bazı öğeler başarısız olmuş olabilir; bu durumda öğe bazında sonuç döndüren özel tasarımlar vardır (toplu işlem endpointi).

Kaynak adlarını Türkçe yazmak hata mı?

Hayır. Türkçe ya da İngilizce adlar ikisi de kullanılabilir; hata, iki dili aynı API'de karıştırmaktır. Türkçe adlarda URL'de ve components anahtarlarında Türkçe karakter kullanmamak da işinizi kolaylaştırır.

Bu hataları otomatik yakalayabilir miyim?

Bir kısmını evet. Fiilli yol, derinlik, eksik 4xx yanıtı ve sayfalama parametresi gibi maddeler OpenAPI belgesi üzerinden taranabilir; kontrol listesi yazısında bunu yapan küçük bir betik var. Gizli alan sızıntısı ve durum kodu kullanımı ise çalışan uygulamaya karşı yazılan testlerle yakalanır.

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 · tasarım kontrol listesi · endpoint isimlendirme · URL derinliği · durum kodu seçimi · hata mesajı formatı · CORS nedir · API güvenliği