Kısa cevap: Bir REST API'de durum kodunu seçerken üç soru sorarsınız: İstek başarılı oldu mu, başarısızsa kusur istemcide mi sunucuda mı, ve istemci bu cevaptan sonra ne yapmalı? Yaygın eşleme şöyledir: Okuma ve güncelleme başarılıysa 200 OK, yeni kaynak oluştuysa Location başlığıyla 201 Created, silme gibi gövdesiz bir başarı için 204 No Content. Bozuk istek 400, kimlik yoksa 401, yetki yoksa 403, kaynak yoksa 404, mevcut durumla çakışma 409, kurallara uymayan veri 422, istek sınırı aşıldıysa 429, beklenmeyen sunucu hatası 500, geçici kapalılık 503.

Bu yazı kodların genel listesini vermiyor; her kodun ne anlama geldiğini ve SEO tarafını HTTP durum kodları rehberinde bulabilirsiniz. Burada aynı küçük kullanıcı ve sipariş API'si üzerinden senaryo bazlı karar veriyoruz: Hangi durumda hangi kod, yanında hangi başlık ve gövde, ve en sık yapılan seçim hataları neler. Tanımlar RFC 9110 (HTTP Semantics) ve RFC 6585 metinlerine dayanıyor. Seri içindeki yeri için REST API tasarımı öğrenme rehberi sayfasına bakabilirsiniz.

Durum kodu bir sözleşmedir

Bir API'yi kullanan program, yanıtı okurken önce durum koduna bakar. İstemci kütüphaneleri, izleme araçları, ara önbellekler ve yeniden deneme mantığı çoğu zaman gövdeyi hiç açmadan bu üç haneli sayıya göre karar verir. Bu yüzden durum kodu, yanıt gövdesindeki mesajdan daha bağlayıcı bir sözleşmedir: Gövde insanlara açıklama yapar, kod ise makineye ne olduğunu söyler.

RFC 9110, kodları ilk hanesine göre sınıflara ayırır. 2xx sınıfı isteğin alındığını, anlaşıldığını ve kabul edildiğini; 4xx sınıfı istemcinin hata yapmış göründüğünü; 5xx sınıfı ise sunucunun hata yaptığını ya da istenen işi yapamadığını bildirir. Standart, 4xx ve 5xx yanıtlarında (HEAD istekleri hariç) sunucunun hatayı açıklayan bir temsil göndermesini ve durumun geçici mi kalıcı mı olduğunu belirtmesini önerir. Aynı metin, kodların yanındaki "Not Found" gibi açıklama ifadelerinin (reason phrase) yalnızca öneri olduğunu, protokolü etkilemeden değiştirilebileceğini de söyler. Yani istemci 404 sayısına güvenmeli, yanındaki metne değil.

Pratik sonuç şudur: Bir istemci geliştiricisi kodunuza bakarak "tekrar denemeli miyim, kullanıcıya bir şey göstermeli miyim, yoksa kendi isteğimi mi düzeltmeliyim" sorusunu cevaplayabilmelidir. 4xx gören istemci aynı isteği değiştirmeden tekrar göndermenin işe yaramayacağını, 5xx ya da 429 gören istemci ise bir süre sonra denemenin anlamlı olabileceğini bilir.

Karar sırası: Kodu hangi adımda seçersiniz?

Karar sırası: Kodu hangi adımda seçersiniz? İstek okunabiliyor mu?, Kim olduğu belli mi?, Bu işe yetkisi var mı?, Hedef kaynak var mı?

Bir isteği işleyen kod genellikle aynı sırayla kontroller yapar. Durum kodunu da bu sıraya bağlarsanız seçim kendiliğinden tutarlı olur:

  • İstek okunabiliyor mu? Gövde geçerli JSON değilse ya da zorunlu bir başlık bozuksa 400.
  • Kim olduğu belli mi? Kimlik bilgisi yoksa veya geçersizse 401. Ayrıntı için 401 ve 403 farkı yazısına bakın.
  • Bu işe yetkisi var mı? Kimlik geçerli ama rol ya da izin yetmiyorsa 403 (ya da kaynağı gizlemek için 404).
  • Hedef kaynak var mı? Yoksa 404. Bu URL üzerinde o metot tanımlı değilse 405.
  • Veri iş kurallarına uyuyor mu? Alan biçimi ya da değer kuralları tutmuyorsa 422.
  • Kaynağın mevcut durumuyla çelişiyor mu? Aynı e-posta zaten kayıtlıysa ya da sipariş iptal edilemez durumdaysa 409.
  • Sınır aşıldı mı? İstemci çok fazla istek gönderdiyse 429.
  • Her şey yolundaysa metoda ve sonuca göre 200, 201 veya 204. Beklenmeyen bir istisna olursa 500, bakım ya da aşırı yük varsa 503.

Sıranın kendisi de bilgi taşır. Örneğin kimliği doğrulanmamış birine önce 422 dönerseniz, yetkisi olmayan birine gövde kurallarınızı öğretmiş olursunuz. Kimlik ve yetki kontrolünü veri doğrulamasından önce yapmak bu yüzden mantıklı bir tasarım tercihidir.

Başarı yanıtları: 200, 201, 204 ve 202

200 OK isteğin başarılı olduğunu söyler ama gövdenin anlamı metoda bağlıdır. RFC 9110'a göre GET yanıtındaki içerik hedef kaynağın temsilidir; POST yanıtında işlemin durumu veya sonucu; PUT ve DELETE yanıtında ise işlemin durumu. 200 yanıtının normalde bir gövde taşıması beklenir; istek başarı durumunda içerik istemediğini belirtiyorsa sunucunun 204 göndermesi gerektiği de aynı bölümde yazar.

201 Created bir veya daha fazla yeni kaynağın oluşturulduğunu bildirir. Oluşan ana kaynak Location başlığıyla gösterilir; başlık yoksa hedef URI kabul edilir. Gövde genellikle oluşan kaynağı anlatır ve ona bağlantı verir. Bu yüzden POST /v1/kullanicilar sonrasında hem yeni kaydı hem de adresini döndürmek istemcinin ikinci bir istek atmasını gereksiz kılar.

http
POST /v1/kullanicilar HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"eposta": "ayse@example.com", "ad": "Ayşe"}
http
HTTP/1.1 201 Created
Location: /v1/kullanicilar/1
Content-Type: application/json

{"id": 1, "eposta": "ayse@example.com", "ad": "Ayşe"}

204 No Content işin yapıldığını ve gönderilecek ek içerik olmadığını anlatır. RFC 9110 açıkça yazar: 204 yanıtı başlık bölümünün sonunda biter, içerik taşıyamaz. Silme işlemleri için doğal bir seçimdir. Bir güncelleme isteğine de 204 dönebilirsiniz; ancak istemci güncel kaydı görmek istiyorsa 200 ile yeni temsili döndürmek bir tur isteği daha kurtarır. Hangi metodun hangi kodla eşleştiği tablosu CRUD endpoint tasarımı yazısında var.

http
HTTP/1.1 204 No Content

Skill'in listesinde yer almayan ama RFC 9110'da tanımlı bir seçenek daha vardır: 202 Accepted. İsteğin işlenmek üzere kabul edildiğini ama işin henüz bitmediğini söyler. Standart bu yanıtı bilerek bağlayıcı olmayan bir yanıt olarak tanımlar; gövdenin işin mevcut durumunu anlatması ve bir durum takip kaynağına işaret etmesi beklenir. Uzun süren rapor üretimi veya toplu içe aktarma gibi işlerde 201 yerine 202 kullanmak daha dürüsttür, çünkü kaynak henüz gerçekten oluşmamıştır.

İstemci hataları: 400, 404, 405, 409, 422

İstemci hataları: 400, 404, 405, 409, 422

400 Bad Request, sunucunun istemci hatası olarak algıladığı bir sebeple isteği işleyemediğini ya da işlemeyeceğini belirtir. RFC 9110 örnek olarak bozuk istek sözdizimini ve geçersiz mesaj çerçevelemeyi sayar. JSON olarak ayrıştırılamayan bir gövde ya da tip olarak hiç anlamlandırılamayan bir parametre bu sınıfa girer.

422 Unprocessable Content (eski adıyla Unprocessable Entity) ise içerik türünün anlaşıldığı, sözdiziminin doğru olduğu ama içindeki talimatların işlenemediği durumu anlatır. Yani JSON geçerlidir, fakat e-posta alanı boştur ya da miktar negatiftir. Skill de doğrulama hataları için 422'yi önerir. İki kodun sınırını nereye çizeceğinizi örneklerle 400 ve 422 farkı yazısında anlatıyoruz; burada önemli olan, seçtiğiniz sınırı bütün uç noktalarda (endpoint) aynı tutmanızdır.

404 Not Found, sunucunun hedef kaynak için güncel bir temsil bulamadığını ya da var olduğunu açıklamak istemediğini söyler. İkinci kısım önemlidir: 404 yalnızca "yok" değil, "size söylemiyorum" anlamına da gelebilir. Kaynak kalıcı olarak kaldırıldıysa ve sunucu bunu biliyorsa RFC 9110 404 yerine 410 Gone kodunu tercih eder.

405 Method Not Allowed, metodun sunucu tarafından bilindiğini ama bu kaynakta desteklenmediğini belirtir. Burada standart zorunlu bir kural koyar: 405 yanıtı, kaynağın o anda desteklediği metotları listeleyen bir Allow başlığı içermek zorundadır.

409 Conflict, isteğin kaynağın mevcut durumuyla çakıştığı için tamamlanamadığını bildirir. RFC 9110 bu kodu kullanıcının çakışmayı çözüp isteği yeniden gönderebileceği durumlar için tarif eder ve sunucunun, çakışmanın kaynağını anlamaya yetecek bilgiyi gövdeye koymasını önerir. Skill'in örneği, zaten kayıtlı bir e-posta adresiyle yeni kullanıcı açma girişimidir. Bu bir doğrulama hatası değildir: Veri kendi başına geçerlidir, sorun sistemdeki başka bir kayıtla çatışmasıdır.

http
HTTP/1.1 409 Conflict
Content-Type: application/json

{"error": "Bu e-posta adresiyle kayıtlı bir kullanıcı var", "code": "EMAIL_ALREADY_EXISTS"}

Gövdedeki code alanı, istemcinin mesaj metnine bakmadan dallanabilmesi için makine okunur bir değerdir. Hata gövdesinin tamamını nasıl tasarlayacağınız hata mesajı formatı yazısının, standart biçim ise Problem Details (RFC 9457) yazısının konusu.

429 Too Many Requests ve Retry-After

429 kodu HTTP'nin ilk sürümlerinde yoktu; RFC 6585 ile eklendi. Belirli bir sürede çok fazla istek gönderildiğini anlatır. RFC 6585, yanıtın durumu açıklayan ayrıntılar içermesini önerir ve ne kadar beklenmesi gerektiğini bildiren bir Retry-After başlığına izin verir. Aynı metin, sunucunun kullanıcıyı nasıl tanıdığını ve istekleri nasıl saydığını tanımlamaz; bu tamamen sizin tasarımınızdır. Bir kural ise kesindir: 429 yanıtları önbellekte saklanmamalıdır.

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": "İstek sınırı aşıldı, 60 saniye sonra tekrar deneyin", "code": "RATE_LIMITED"}

Retry-After RFC 9110'da tanımlıdır ve değeri ya bir HTTP tarihi ya da negatif olmayan bir saniye sayısıdır. Skill'de geçen X-RateLimit-Limit gibi başlıklar ise bir RFC standardı değil, bir gelenektir (convention). Algoritmalar ve bu başlıklar API rate limiting stratejileri yazısında ayrıntılı olarak anlatılıyor.

Sunucu hataları: 500 ve 503

500 Internal Server Error, sunucunun isteği yerine getirmesini engelleyen beklenmeyen bir durumla karşılaştığını söyler. Yakalanmamış bir istisna, bozulan bir veritabanı bağlantısı ya da koddaki bir hata bu sınıfa girer. 500 dönmek istemciye "senin hatan değil" der; bu yüzden geçerli bir isteğe verilmiş bir doğrulama cevabını 500 olarak göndermek istemciyi yanlış yönlendirir. Gövdeye yığın izi (stack trace) veya iç sorgu metni koymayın; bunlar saldırgan için bilgi, istemci için gürültüdür.

503 Service Unavailable, sunucunun geçici bir aşırı yük ya da planlı bakım nedeniyle isteği şu anda işleyemediğini ve durumun bir süre sonra düzelmesinin beklendiğini anlatır. RFC 9110, sunucunun ne kadar beklenmesi gerektiğini önermek için Retry-After gönderebileceğini söyler. 500 ile farkı budur: 503 "şu an olmaz, sonra dene", 500 ise "beklenmeyen bir şey oldu" demektir.

http
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: application/json

{"error": "Planlı bakım çalışması sürüyor", "code": "MAINTENANCE"}

Hata için 200 dönmeyin

Kaçınılması gereken bir tasarım hatası, her yanıtı 200 ile gönderip başarıyı gövdedeki bir alanla anlatmaktır:

json
{"success": false, "error": "Kullanıcı bulunamadı"}

Bu yaklaşım ilk bakışta basit görünür ama sözleşmeyi bozar. İstemci her yanıtın gövdesini açıp özel bir alanı okumak zorunda kalır; genel amaçlı HTTP istemcileri, izleme panelleri ve yük dengeleyiciler bu yanıtları başarılı sayar. Daha sinsi bir sorun önbellektir: RFC 9110'a göre 200 yanıtları sezgisel olarak önbelleğe alınabilir (heuristically cacheable). Açık bir önbellek talimatı yoksa, hata anlatan bir 200 yanıtının bir ara katmanda saklanıp başka isteklere de sunulma ihtimali doğar. Doğru yol, hatayı uygun 4xx ya da 5xx koduyla göndermek ve gövdeyi açıklama için kullanmaktır.

Senaryoya göre hızlı seçim tablosu

Senaryoya göre hızlı seçim tablosu: Kullanıcı listesi veya tek kayıt okundu, POST ile yeni sipariş oluştu, Uzun süren dışa aktarma işi kuyruğa alındı, Sipariş silindi
SenaryoKodYanında
Kullanıcı listesi veya tek kayıt okundu200 OKKaynağın temsili
POST ile yeni sipariş oluştu201 CreatedLocation başlığı ve yeni kayıt
Uzun süren dışa aktarma işi kuyruğa alındı202 Acceptedİşin durumunu izleyen kaynağa bağlantı
Sipariş silindi204 No ContentGövde yok
Gövde geçerli JSON değil400 Bad RequestHatanın açıklaması
Authorization başlığı yok veya token süresi dolmuş401 UnauthorizedWWW-Authenticate başlığı
Üye rolündeki kullanıcı admin işlemi denedi403 ForbiddenGerekçe (isteğe bağlı)
Sipariş numarası yok404 Not FoundHatanın açıklaması
Bu adreste DELETE tanımlı değil405 Method Not AllowedAllow başlığı (zorunlu)
E-posta zaten kayıtlı409 ConflictÇakışmayı anlatan bilgi
Miktar alanı negatif422 Unprocessable ContentAlan bazında hata listesi
Dakikalık istek sınırı aşıldı429 Too Many RequestsRetry-After (isteğe bağlı)
Yakalanmamış istisna500 Internal Server Errorİç ayrıntı içermeyen açıklama
Planlı bakım503 Service UnavailableRetry-After (isteğe bağlı)

FastAPI ile çalışan bir örnek

Aşağıdaki küçük uygulama, kullanıcı oluşturma, okuma ve silme işlemlerinde kodları açıkça seçiyor. Veritabanı yerine bellekteki bir sözlük kullanılıyor; gerçek bir uygulamada bu kalıcı bir depo olur. Uygulamayı ağ portu açmadan TestClient ile sürdük ve yorum satırlarındaki çıktılar gerçek çalıştırmadan alındı.

python
from fastapi import FastAPI, HTTPException, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel

app = FastAPI()
kullanicilar = {}  # gerçek uygulamada veritabanı

class KullaniciGiris(BaseModel):
    eposta: str
    ad: str

@app.post("/v1/kullanicilar", status_code=201)
def olustur(veri: KullaniciGiris, response: Response):
    if any(k["eposta"] == veri.eposta for k in kullanicilar.values()):
        raise HTTPException(status_code=409, detail="Bu e-posta zaten kayıtlı")
    yeni_id = len(kullanicilar) + 1
    kullanicilar[yeni_id] = {"id": yeni_id, **veri.model_dump()}
    response.headers["Location"] = f"/v1/kullanicilar/{yeni_id}"
    return kullanicilar[yeni_id]

@app.get("/v1/kullanicilar/{kullanici_id}")
def getir(kullanici_id: int):
    if kullanici_id not in kullanicilar:
        raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı")
    return kullanicilar[kullanici_id]

@app.delete("/v1/kullanicilar/{kullanici_id}", status_code=204)
def sil(kullanici_id: int):
    if kullanicilar.pop(kullanici_id, None) is None:
        raise HTTPException(status_code=404, detail="Kullanıcı bulunamadı")

istemci = TestClient(app)
r = istemci.post("/v1/kullanicilar", json={"eposta": "ayse@example.com", "ad": "Ayşe"})
print(r.status_code, r.headers["location"], r.json())
# çıktı: 201 /v1/kullanicilar/1 {'id': 1, 'eposta': 'ayse@example.com', 'ad': 'Ayşe'}
r = istemci.post("/v1/kullanicilar", json={"eposta": "ayse@example.com", "ad": "Ayşe"})
print(r.status_code, r.json())
# çıktı: 409 {'detail': 'Bu e-posta zaten kayıtlı'}
r = istemci.post("/v1/kullanicilar", json={"ad": "Ali"})
print(r.status_code)
# çıktı: 422
r = istemci.delete("/v1/kullanicilar/1")
print(r.status_code, repr(r.content))
# çıktı: 204 b''
r = istemci.get("/v1/kullanicilar/1")
print(r.status_code, r.json())
# çıktı: 404 {'detail': 'Kullanıcı bulunamadı'}
r = istemci.put("/v1/kullanicilar/1", json={})
print(r.status_code, r.headers.get("allow"), r.json())
# çıktı: 405 GET {'detail': 'Method Not Allowed'}

Çıktıları sırayla okuyalım. İlk POST, dekoratördeki status_code=201 sayesinde 201 döner ve Location başlığını taşır. Aynı e-postayla ikinci deneme 409 alır, çünkü veri geçerli ama mevcut kayıtla çakışıyor. Zorunlu eposta alanı eksik olan istek, uç nokta fonksiyonu hiç çalışmadan FastAPI'nin kendi doğrulamasıyla 422 alır. Silme 204 döner ve gövde gerçekten boştur. Silinen kaydı okumaya çalışmak 404 verir.

Son satır küçük ama öğretici bir ayrıntı gösteriyor: Bu adreste PUT tanımlı olmadığı için 405 dönüyor ve Allow başlığı gönderiliyor. Ancak denediğimiz sürümde başlıkta yalnızca GET görünüyor, oysa aynı yolda DELETE de tanımlı. RFC 9110, Allow değerinin kaynağın desteklediği metotların listesi olmasını istediği için, bu başlığa güvenen istemcileriniz varsa kendi çatınızın davranışını bir testle kontrol etmeniz iyi olur.

Sık yapılan hatalar

  • Her hatayı 400 veya 500 ile göndermek. İstemci, kimlik sorunu ile veri sorununu ayıramaz. En azından 401, 403, 404, 409 ve 422 ayrımını yapın.
  • Oluşturma işleminde 200 dönmek. 201 ve Location, istemciye yeni kaynağın adresini standart bir yoldan verir.
  • 204 yanıtına gövde eklemek. Standarda göre 204 içerik taşıyamaz; gövde göndermek istiyorsanız 200 kullanın.
  • Kimlik eksikliğinde 403 dönmek. Kimlik yoksa doğru kod 401'dir ve WWW-Authenticate başlığı zorunludur.
  • Doğrulama hatasını 500 olarak sızdırmak. Kontrol edilmeyen bir değer istisnaya yol açıyorsa sorun istemcinin verisindedir; bunu yakalayıp 422 ya da 400 olarak döndürün.
  • Yeniden denenebilir hatayı belirsiz bırakmak. 429 ve 503 yanıtlarında Retry-After göndermek, istemcilerin sunucuyu gereksiz yere zorlamasını azaltır. Ödeme gibi tekrar edilmesi riskli istekler için Idempotency-Key yazısına bakın.

Sık Sorulan Sorular

Güncelleme sonrası 200 mü 204 mü dönmeliyim?

İkisi de geçerlidir. Güncel kaynağı gövdede döndürüyorsanız 200, hiçbir şey döndürmüyorsanız 204 kullanın. İstemciler çoğu zaman sunucunun eklediği alanları (örneğin güncelleme zamanı) görmek istediği için 200 ile yeni temsili döndürmek pratik bir tercihtir. Önemli olan, aynı tür işlemde her yerde aynı seçimi yapmaktır.

Arama sonucu boşsa 404 mü dönmeliyim?

Hayır. GET /v1/kullanicilar?role=admin isteğinin hedefi koleksiyondur ve koleksiyon vardır; yalnızca filtreye uyan kayıt yoktur. Boş bir liste ile 200 dönmek doğru olandır. 404, hedef kaynağın kendisi bulunamadığında kullanılır, örneğin /v1/kullanicilar/999 gibi.

Reason phrase metnini Türkçe yazabilir miyim?

RFC 9110, açıklama ifadelerinin yalnızca öneri olduğunu ve yerel karşılıklarla değiştirilebileceğini ya da tamamen çıkarılabileceğini söyler. Yine de istemciler bu metne değil koda bakmalıdır. Kullanıcıya gösterilecek Türkçe mesajı gövdeye koymak daha güvenilir bir yoldur.

Kendi durum kodumu, örneğin 499'u uydurabilir miyim?

RFC 9110, standart dışındaki kodların HTTP durum kodu kaydına tescil edilmesi gerektiğini söyler. Aynı metne göre istemci her kodun sınıfını anlamak ve tanımadığı bir kodu o sınıfın x00 koduna eşdeğer saymak zorundadır; tanımadığı 499 kodunu 400 gibi ele alır. Uygulamaya özel ayrımları yeni bir kodla değil, gövdedeki makine okunur bir hata koduyla yapmak daha taşınabilir bir çözümdü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 · HTTP durum kodları rehberi · 401 ve 403 farkı · 400 ve 422 farkı · Problem Details hata yanıtı · CRUD endpoint tasarımı · API rate limiting · HTTP istek ve yanıt yapısı