İyi bir API hata mesajı formatı üç şeyi aynı anda verir: insan için kısa bir açıklama, program için değişmeyen bir hata kodu (örneğin VALIDATION_ERROR) ve gerekiyorsa hangi alanın neden reddedildiği. İncelediğimiz tasarım rehberi bunun için iki kalıp önerir: basit durumlar için yalnızca error ve code taşıyan minimal bir gövde, karmaşık durumlar için message, fields, timestamp ve path alanları eklenmiş ayrıntılı bir gövde. Hangisini seçerseniz seçin, asıl kural bu biçimin tüm uç noktalarda ve tüm hata türlerinde aynı olmasıdır.

Standart bir biçim isteyenler için RFC 9457 Problem Details vardır ve Problem Details yazısında ayrıntılı anlatılır. Bu yazı, kendi biçimini tanımlamak isteyen ya da mevcut özel biçimini düzene sokmak isteyen ekipler için. Hata kodlarının nasıl adlandırılacağını, alan hatalarının nasıl gösterileceğini ve çatının (framework) kendi ürettiği 404, 405 ve 500 yanıtlarının da aynı biçime nasıl sokulacağını çalışan bir FastAPI örneğiyle gösteriyoruz.

Minimal biçim: error ve code

En sade hata gövdesi iki alandan oluşur:

json
{
  "error": "E-posta adresi geçersiz",
  "code": "VALIDATION_ERROR"
}

error bir insanın okuyacağı kısa açıklamadır; arayüzde doğrudan gösterilebilir ya da günlüğe yazılabilir. code ise programın karar vermek için kullanacağı sabit bir değerdir. İkisini ayırmanın nedeni basittir: metinler zamanla düzeltilir, çevrilir, yeniden yazılır; istemci kodu bunlara bağlı olursa her metin değişikliği istemciyi bozar. Kod ise bir kez yayımlandıktan sonra değişmez. RFC 9457 de aynı ilkeyi kendi alanları için koyar: istemcilerin insan için yazılmış açıklama metnini ayrıştırarak bilgi çıkarmaya çalışmaması gerektiğini söyler.

Minimal biçim, ek ayrıntıya ihtiyaç olmayan hatalar için yeterlidir: bulunamayan kaynak, yetkisiz erişim, desteklenmeyen metot gibi. Bu durumlarda istemcinin bilmesi gereken tek şey ne olduğudur, nerede olduğu değil.

Ayrıntılı biçim: message, fields, timestamp, path

Ayrıntılı biçim: message, fields, timestamp, path: error, code, message, fields

Doğrulama gibi birden çok alanı ilgilendiren hatalarda daha fazla bilgi gerekir. İncelediğimiz rehberin ayrıntılı kalıbının Türkçe uyarlaması şöyle:

json
{
  "error": "Doğrulama başarısız",
  "code": "VALIDATION_ERROR",
  "message": "Bir veya daha fazla alan doğrulamadan geçemedi",
  "fields": {
    "email": "Geçerli bir e-posta adresi olmalı",
    "parola": "En az 8 karakter olmalı"
  },
  "timestamp": "2026-11-07T09:00:00Z",
  "path": "/kullanicilar"
}
  • error: hatanın kısa özeti, minimal biçimdekiyle aynı alan. Böylece istemci her iki biçimde de aynı alanı okuyabilir.
  • code: makine okunur hata kodu. Durum kodundan daha ayrıntılı olabilir; 409 yanıtı hem EMAIL_TAKEN hem de başka bir çakışma kodu taşıyabilir.
  • message: daha uzun açıklama, isteğe bağlı. Kullanıcıya ne yapması gerektiğini anlatmak için uygundur.
  • fields: alan adından o alanın hata mesajına giden bir nesne. İstemci formdaki her alanın altına ilgili mesajı yerleştirebilir.
  • timestamp: hatanın oluştuğu an. RFC 3339 biçiminde ve UTC olarak yazılması, günlüklerle eşleştirmeyi kolaylaştırır; ayrıntılar alan adlandırma ve tarih formatı yazısında.
  • path: hatanın oluştuğu istek yolu. Destek talebinde kullanıcının hangi işlemi yaptığını anlamaya yardım eder.

Bu alanların hiçbiri bir standart tarafından tanımlanmaz; adlarını ve anlamlarını siz belirler ve belgelersiniz. fields içinde iç içe alanlar için bir yazım kuralı da seçmeniz gerekir. Aşağıdaki örnekteki işleyici iç içe alanları adres.il gibi noktalı bir yol olarak yazıyor; RFC 9457'nin doğrulama örneğindeki gibi JSON Pointer (#/adres/il) da bir seçenektir. Alan adlarının kendisi de istek gövdesindeki adlarla birebir aynı olmalıdır, yoksa istemci mesajı doğru alana yerleştiremez.

Hata kodlarını tasarlamak

Hata kodları API sözleşmesinin parçasıdır ve ona göre tasarlanmalıdır. İncelediğimiz rehberin örneği VALIDATION_ERROR biçimini, yani büyük harf ve alt çizgiyle yazılmış İngilizce bir sabiti kullanır. Bu yazım, kodların JSON anahtarlarından ve insan metinlerinden görsel olarak ayrılmasını sağlar ve istemci dillerinde sabit adı olarak doğrudan kullanılabilir. Kodları İngilizce, mesajları Türkçe tutmak da yaygın bir ayrımdır: kod geliştirici için, mesaj kullanıcı içindir.

Bir kod kataloğu, her kodun hangi HTTP durum koduyla birlikte kullanıldığını da belgeler:

Hata koduHTTP durum koduNe zaman
VALIDATION_ERROR422 Unprocessable ContentGövde ya da parametre kurallara uymuyor
INVALID_JSON400 Bad RequestGövde JSON olarak ayrıştırılamıyor
UNAUTHORIZED401 UnauthorizedKimlik bilgisi yok ya da geçersiz
FORBIDDEN403 ForbiddenKimlik doğrulandı ama yetki yok
NOT_FOUND404 Not FoundKaynak ya da yol yok
METHOD_NOT_ALLOWED405 Method Not AllowedYol var ama metot desteklenmiyor
EMAIL_TAKEN409 ConflictE-posta başka bir hesapta kayıtlı
RATE_LIMITED429 Too Many Requestsİstek sınırı aşıldı
INTERNAL_ERROR500 Internal Server ErrorBeklenmeyen sunucu hatası

Bu katalogda iki tasarım kararı görülüyor. Birincisi, kodlar durum kodlarının yerine geçmez, onları inceltir: EMAIL_TAKEN yine 409 ile döner, çünkü proxy'ler, istemci kütüphaneleri ve izleme araçları gövdeyi değil durum kodunu okur. Hata için 200 dönüp gövdede kod göndermek bu araçların hepsini yanıltır; senaryoya göre kod seçimi durum kodu seçimi yazısında. İkincisi, kodlar ne kadar ayrıntılı olursa istemci o kadar isabetli davranır, ama her yeni kod belgelenmesi ve bakımı gereken bir sözleşme maddesidir. İstemcinin farklı tepki vermesi gerekmeyen durumlar için ayrı kod üretmeyin.

429 satırındaki RATE_LIMITED gibi kodlar, ilgili başlıklarla birlikte anlam kazanır; hız sınırlama başlıkları ve algoritmaları API rate limiting stratejileri yazısında anlatılıyor. 401 ile 403 arasındaki ayrım ise 401 ve 403 farkı yazısının konusu.

Özel biçim mi, Problem Details mı?

Özel biçim mi, Problem Details mı? Tanım, Medya türü, Makine okunur kimlik, Alan hataları
ÖlçütÖzel biçim (error, code)Problem Details (RFC 9457)
TanımAPI belgenizdeIETF standardı
Medya türüapplication/jsonapplication/problem+json
Makine okunur kimlikcode (sabit metin)type (URI)
Alan hatalarıfields nesnesiUzantı alanı (örneğin errors dizisi)
Olay kimliğiSiz eklersinizinstance üyesi
Genel araç desteğiSize özelBiçimi bilen her istemci

İki yaklaşım birbirini dışlamaz. RFC 9457 uzantı alanlarına izin verdiği için, alışık olduğunuz code ve fields alanlarını bir Problem Details nesnesine ekleyebilirsiniz. Tanımadığı uzantıları yok saymak istemciler için bir zorunluluk olduğundan, yalnızca standardı bilen istemci de bu yanıtı sorunsuz işler:

json
{
  "type": "https://api.example.com/problemler/dogrulama-hatasi",
  "title": "Doğrulama başarısız",
  "status": 422,
  "detail": "Bir veya daha fazla alan doğrulamadan geçemedi",
  "code": "VALIDATION_ERROR",
  "fields": {
    "email": "Geçerli bir e-posta adresi olmalı"
  }
}

Mevcut bir API'de özel biçimden Problem Details'a geçmek ise gövdenin şeklini değiştirdiği için kırıcı bir değişikliktir. Böyle bir geçişi yeni bir sürümle ya da istemcinin Accept başlığıyla tercih bildirmesiyle yapmak gerekir; RFC 9457 de mevcut API'lere Problem Details desteğinin içerik anlaşmasıyla eklenebileceğini söyler.

Her hata aynı biçimde: FastAPI örneği

Tutarlılık en çok, hatayı sizin kodunuzun değil çatının ürettiği yerlerde bozulur. FastAPI varsayılan olarak tanımsız bir yol için {"detail": "Not Found"}, desteklenmeyen bir metot için {"detail": "Method Not Allowed"}, doğrulama hatası için ise detail dizisi döndürür. Sizin uç noktalarınız ayrıntılı biçimi kullanırken bu üç yanıt farklı şekilde gelirse, istemci yine iki ayrı ayrıştırma yolu yazmak zorunda kalır. Çözüm, dört istisna işleyicisi (exception handler) tanımlamaktır: kendi hata sınıfınız, doğrulama hataları, çatının HTTP hataları ve beklenmeyen istisnalar. Örnekte zaman damgası çıktı sabit kalsın diye sabit bir fonksiyondan geliyor; kayıtlı e-postalar ise veritabanı yerine bellekteki bir kümede:

python
from datetime import datetime, timezone
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from starlette.exceptions import HTTPException as StarletteHTTPException

app = FastAPI()

def simdi():
    # Üretimde: datetime.now(timezone.utc). Örnekte çıktı sabit kalsın diye sabit zaman.
    return datetime(2026, 11, 7, 9, 0, tzinfo=timezone.utc)

class ApiHatasi(Exception):
    def __init__(self, status: int, code: str, error: str, message: str | None = None, fields: dict | None = None):
        self.status, self.code, self.error, self.message, self.fields = status, code, error, message, fields

def hata_yaniti(request: Request, status, code, error, message=None, fields=None, headers=None):
    govde = {"error": error, "code": code}
    if message:
        govde["message"] = message
    if fields:
        govde["fields"] = fields
    govde["timestamp"] = simdi().isoformat().replace("+00:00", "Z")
    govde["path"] = request.url.path
    return JSONResponse(status_code=status, content=govde, headers=headers)

@app.exception_handler(ApiHatasi)
async def api_hatasi(request: Request, exc: ApiHatasi):
    return hata_yaniti(request, exc.status, exc.code, exc.error, exc.message, exc.fields)

@app.exception_handler(RequestValidationError)
async def dogrulama(request: Request, exc: RequestValidationError):
    alanlar = {".".join(str(x) for x in e["loc"][1:]) or e["loc"][0]: e["msg"] for e in exc.errors()}
    return hata_yaniti(request, 422, "VALIDATION_ERROR", "Doğrulama başarısız",
                       "Bir veya daha fazla alan doğrulamadan geçemedi", alanlar)

KODLAR = {404: ("NOT_FOUND", "Kaynak bulunamadı"), 405: ("METHOD_NOT_ALLOWED", "Bu metot desteklenmiyor")}

@app.exception_handler(StarletteHTTPException)
async def http_hatasi(request: Request, exc: StarletteHTTPException):
    code, error = KODLAR.get(exc.status_code, ("HTTP_ERROR", str(exc.detail)))
    return hata_yaniti(request, exc.status_code, code, error, headers=exc.headers)

@app.exception_handler(Exception)
async def beklenmeyen(request: Request, exc: Exception):
    # Ayrıntıyı günlüğe yaz, istemciye gönderme
    return hata_yaniti(request, 500, "INTERNAL_ERROR", "Beklenmeyen bir sunucu hatası oluştu")

class KullaniciOlustur(BaseModel):
    email: str = Field(min_length=6)
    parola: str = Field(min_length=8)

KAYITLI = {"ayse@example.com"}

@app.post("/kullanicilar", status_code=201)
def olustur(k: KullaniciOlustur):
    if k.email in KAYITLI:
        raise ApiHatasi(409, "EMAIL_TAKEN", "Bu e-posta zaten kayıtlı", fields={"email": "Başka bir adres deneyin"})
    return {"id": 43, "email": k.email}

@app.get("/rapor")
def rapor():
    return 1 / 0  # beklenmeyen hata

c = TestClient(app, raise_server_exceptions=False)
print(c.post("/kullanicilar", json={"email": "can", "parola": "123"}).json())
# çıktı: {'error': 'Doğrulama başarısız', 'code': 'VALIDATION_ERROR', 'message': 'Bir veya daha fazla alan doğrulamadan geçemedi', 'fields': {'email': 'String should have at least 6 characters', 'parola': 'String should have at least 8 characters'}, 'timestamp': '2026-11-07T09:00:00Z', 'path': '/kullanicilar'}
r = c.post("/kullanicilar", json={"email": "ayse@example.com", "parola": "uzun-parola"})
print(r.status_code, r.json()["code"], r.json()["fields"])
# çıktı: 409 EMAIL_TAKEN {'email': 'Başka bir adres deneyin'}
r = c.get("/siparisler")
print(r.status_code, r.json()["code"], r.json()["path"])
# çıktı: 404 NOT_FOUND /siparisler
r = c.delete("/kullanicilar")
print(r.status_code, r.headers["allow"], r.json()["code"])
# çıktı: 405 POST METHOD_NOT_ALLOWED
r = c.get("/rapor")
print(r.status_code, r.json())
# çıktı: 500 {'error': 'Beklenmeyen bir sunucu hatası oluştu', 'code': 'INTERNAL_ERROR', 'timestamp': '2026-11-07T09:00:00Z', 'path': '/rapor'}

Beş çıktının hepsi aynı iskeleti taşıyor. Doğrulama hatasında her alanın mesajı fields içinde; Pydantic'in İngilizce mesajlarını isterseniz hata türüne göre Türkçeleştirebilirsiniz. İş kuralı ihlali 409 ve EMAIL_TAKEN ile dönüyor. Tanımsız /siparisler yolu ve /kullanicilar üzerindeki DELETE artık çatının varsayılan gövdesini değil, sizin biçiminizi kullanıyor; 405 yanıtındaki Allow başlığı da işleyicide exc.headers aktarıldığı için korunuyor. Bu başlık hangi metotların desteklendiğini söylediği için kaybolmaması önemlidir.

İşleyiciyi FastAPI'nin değil Starlette'in HTTPException sınıfına bağlamanın bir nedeni var: FastAPI'nin HTTPException sınıfı Starlette'inkinden türetilmiştir ve yönlendirici, eşleşmeyen yollar için Starlette istisnasını kullanır. Üst sınıfa bağlanan işleyici ikisini birden yakalar. Son çıktıda sıfıra bölme hatası 500 ile, ama yalnızca genel bir mesajla dönüyor; istisnanın türü ya da yığın dökümü gövdeye girmiyor. Örnekteki raise_server_exceptions=False ayarı yalnızca TestClient'ın istisnayı test kodunda yeniden fırlatmasını engellemek içindir.

5xx yanıtlarında da gövde boş bırakılmamalıdır. RFC 9110, sunucunun HEAD dışındaki isteklere verdiği hata yanıtlarında durumu açıklayan ve geçici mi kalıcı mı olduğunu belirten bir temsil göndermesi gerektiğini söyler (SHOULD); 4xx sınıfı için de aynı öneri vardır. Açıklamanın iç ayrıntı değil, istemcinin anlayacağı bir durum bilgisi olması yeterlidir.

Sık yapılan hatalar

  • Uç noktaya göre değişen biçim: bir yerde {"error": ...}, başka yerde {"message": ...}, çatının ürettiği yanıtlarda {"detail": ...}. İstemci üç ayrı yol yazar.
  • Kodsuz hata: yalnızca metin döndürmek, istemciyi metni karşılaştırmaya zorlar; metin değiştiğinde istemci bozulur.
  • Kodu mesaj gibi yazmak: "code": "E-posta zaten var" gibi bir değer sabit değil, insan metnidir. Kodlar kısa, ASCII ve değişmez olmalıdır.
  • Durum kodunu yok saymak: her hatayı 400 ya da 200 ile göndermek ve farkı yalnızca code alanına bırakmak, HTTP'yi anlayan tüm araçları devre dışı bırakır.
  • İç ayrıntı sızdırmak: SQL hatası, dosya yolu, istisna türü ya da yığın dökümü hata gövdesine girmemelidir. İncelediğimiz rehberin kontrol listesi yanıtlarda sır bulunmamasını ayrı bir madde olarak sayar.
  • fields içinde istekten farklı adlar: gövdede parola gönderilirken hatada password kullanmak, mesajın ilgili alana bağlanmasını imkânsız kılar.
  • Belgesiz kodlar: kataloğu olmayan bir kod listesi, istemci geliştiricisinin kodları deneme yanılmayla keşfetmesine yol açar. Hata yanıtlarını OpenAPI belgesinde de tanımlayın; bkz. OpenAPI ve Swagger ile dokümantasyon.

Sık Sorulan Sorular

Minimal ve ayrıntılı biçimi aynı API'de kullanabilir miyim?

Evet, ayrıntılı biçim minimal biçimin bir üst kümesi olduğu sürece. Her yanıtta error ve code bulunur; fields gibi alanlar yalnızca gerektiğinde eklenir. İstemci her zaman aynı iki alana güvenebilir.

Hata kodları sayı mı olmalı, metin mi?

Metin kodlar (EMAIL_TAKEN) kendini açıklar ve günlükte okunabilir; sayısal kodlar (1042) her seferinde bir tabloya bakmayı gerektirir. Hangisini seçerseniz seçin, değerleri belgeleyin ve değiştirmeyin.

Hata mesajlarını kullanıcının diline göre değiştirmeli miyim?

Bu bir tasarım tercihidir. Mesajı yerelleştirirken kodu sabit tutmanız yeterlidir; istemci kararlarını koda göre verir, mesajı yalnızca gösterir. Problem Details kullanıyorsanız RFC 9457, insan için yazılmış metinlerin dilinin Accept-Language başlığıyla anlaşılabileceğini belirtir.

Doğrulama hatalarında tüm alanları mı, yalnızca ilkini mi döndürmeliyim?

Tümünü döndürmek kullanıcının formu tek turda düzeltmesini sağlar. Pydantic zaten tüm alanları denetleyip hataları liste olarak verir; örnekteki işleyici bu listeyi fields nesnesine çeviriyor. 400 ile 422 arasındaki seçim için istek doğrulama yazısına bakın.

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 · Problem Details (RFC 9457) · 400 ve 422 farkı · Durum kodu seçimi · Breaking change · REST API güvenlik rehberi