Birlikte çalıştığım her takım eninde sonunda kendi API cevap formatını icat eder. Başta zararsız
görünür — burada küçük bir wrapper objesi, orada özel bir hata şekli — ve sonra altı ay sonra hata ayrıştırma
middleware'inin dördüncü sürümünü yazıyorsun ve code review'da data.user mu yoksa
data.result.user mı "doğru" yol diye tartışıyorsun. Bunların hepsini çözen evrensel bir standart
yok, ama production'da ayakta kalan pattern'ler ve kesinlikle seni ısırmak için geri gelecek antipattern'ler
var. Gerçekten bir design doc'a koyacaklarım bunlar.
Tutarlı Success Cevapları
Her takımın tartıştığı ilk soru: her cevap {"status": "ok", "data": {...}} gibi bir
envelope'a sarılmalı mı? Dürüst cevap: büyük ihtimalle varsayılan olarak hayır. Envelope'lar, HTTP status
kodlarının proxy'ler ve mobil ağlar arasında her zaman güvenilir olmadığı 2000'lerin başında daha mantıklıydı.
Bugün, kaynağın kendi adına konuşmasına izin veren düz bir cevap neredeyse her zaman daha temizdir. Envelope'ı
gerçekten karışık payload döndüren endpoint'ler için sakla, örneğin kısmen başarılı olan bir bulk işlemi.
// ✅ Good — flat, direct, the order IS the response
// GET /v1/orders/ord_9kZ2m
{
"id": "ord_9kZ2m",
"status": "fulfilled",
"customer_id": "cus_4xA1p",
"total_amount": 149.99,
"currency": "USD",
"created_at": "2026-03-15T11:42:00Z",
"line_items": [
{ "sku": "HDPHN-BLK-XM5", "quantity": 1, "unit_price": 149.99 }
]
}
// ❌ Avoid — unnecessary envelope adds a layer clients have to unwrap every time
{
"status": "success",
"code": 200,
"data": {
"order": {
"id": "ord_9kZ2m"
}
}
}Sarmalama, kaynağın kendisinin parçası olmayan metadata'yı bir arada tutman gerektiğinde mantıklıdır —
paginasyon cursor'ları, tracing için request ID'leri veya bulk endpoint'lerde kısmi başarısızlık özetleri.
Basit bir GET /orders/:id için sipariş cevaptır. response.id gayet iyi çalışırken
client'lara response.data.order.id yazdırma. Referans alacak bir spec istiyorsan,
JSON:API, envelope'ları ne zaman ve nasıl
kullanacağını tam olarak tanımlayan fikirli ama iyi düşünülmüş bir standarttır — tamamını benimsemesen bile
okumaya değer.
Hata Cevapları — RFC 7807 Problem Details Kullan
Özel hata şekilleri, entegrasyon acısının en yaygın kaynaklarından biridir. Her API biraz farklı
bir şeyle son bulur — {"error": "..."}, {"message": "...", "code": 42},
{"errors": [...]} — ve API'ni kullanan her client özel hata ayrıştırma mantığı yazmak zorundadır.
IETF bunu
RFC 7807 — HTTP API'leri için Problem Details
ile çözdü. Hatalar için tutarlı bir JSON yapısı tanımlayan, Content-Type'ı
application/problem+json olan hafif bir standarttır. Benimse ve hata formatın herhangi bir
geliştiricinin dokümanlara uzanmadan okuyabileceği bir şey olur.
// POST /v1/orders — 422 Unprocessable Entity
// Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation Failed",
"status": 422,
"detail": "The order could not be created because one or more fields are invalid.",
"instance": "/v1/orders/requests/req_7bN3k",
"errors": [
{
"field": "line_items[0].quantity",
"message": "Quantity must be a positive integer."
},
{
"field": "shipping_address.postal_code",
"message": "Postal code is required for US shipments."
}
]
}- Öngörülebilir ayrıştırma: Client'lar insan tarafından okunabilir mesajı (
detail), makine tarafından okunabilir kategoriyi (type) ve body'de yansıyan HTTP status'ünü (status) her zaman nerede bulacaklarını bilir. - Tasarım gereği genişletilebilir: Spec, alan-seviyesi doğrulama ayrıntısı için
errorsgibi ekstra alanlara açıkça izin verir — etrafından dolanmıyorsun. - Araç desteği: OpenAPI 3.x,
application/problem+json'ı bir cevap content type'ı olarak destekler, dolayısıyla oluşturulan docs'ların ve client SDK'ların şekli doğal olarak anlar. typeURI'si sadece bir string değil, bir belgedir: Hatayı açıklayan gerçek bir sayfaya yönlendir ve bir destek biletini self-service cevapla değiştirmiş olursun.
HTTP Status Kodları + JSON Body Birlikte
Status kodu ve JSON body birbirinin tekrarı değildir — farklı roller oynarlar. Status kodu HTTP katmanına (proxy'ler, cache'ler, tarayıcılar, monitoring araçları) ne olduğunu söyler. JSON body senin application katmanına söyler. İkisi de doğru olmalı. MDN'in HTTP status referansı hangi kodun uygun olduğu konusundaki tartışmaları çözmenin en hızlı yoludur. Takımların en sık takıldıkları kodlar 400 vs 422 (her ikisi de client hatasıdır, ancak 422 özellikle sözdiziminin geçerli olduğunu ve sunucunun onu anladığını — semantiklerin yanlış olduğunu ima eder) ve 401 vs 403 (401 "kimsin?" demektir, 403 "kim olduğunu biliyorum — bunu yapamazsın" demektir).
// 400 Bad Request — malformed JSON or missing required field at the HTTP level
{
"type": "https://api.example.com/problems/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "Request body is not valid JSON."
}
// 422 Unprocessable Entity — valid JSON, but business rules rejected it
{
"type": "https://api.example.com/problems/insufficient-inventory",
"title": "Insufficient Inventory",
"status": 422,
"detail": "HDPHN-BLK-XM5 has 0 units available; requested 2.",
"instance": "/v1/orders/requests/req_7bN3k"
}
// 404 Not Found — resource doesn't exist (or you don't want to reveal it does)
{
"type": "https://api.example.com/problems/not-found",
"title": "Order Not Found",
"status": 404,
"detail": "No order with ID ord_XXXXX exists in this account."
}- 200 OK — body döndüren başarılı GET, PUT, PATCH
- 201 Created — kaynak oluşturan başarılı POST; yeni kaynağa işaret eden bir
Locationheader'ı ekle - 204 No Content — cevap body'si olmayan başarılı DELETE veya eylem; JSON gerekmez
- 400 Bad Request — bozuk request sözdizimi, sunucu onu parse bile edemiyor
- 401 Unauthorized — eksik veya geçersiz kimlik doğrulama credential'ları
- 403 Forbidden — kimlik doğrulaması yapılmış ama izinli değil
- 404 Not Found — kaynak mevcut değil
- 409 Conflict — durum çakışması (örn. yinelenen sipariş, optimistic lock başarısızlığı)
- 422 Unprocessable Entity — sözdizimi geçerli, semantik/iş doğrulaması başarısız
- 429 Too Many Requests — rate limit aşıldı; her zaman bir
Retry-Afterheader'ı ekle - 500 Internal Server Error — sunucu tarafında bir şey bozuldu; body'de asla stack trace sızdırma
Tarihler ve Saatler — Her Zaman ISO 8601
Unix timestamp'ler temiz görünür — sadece bir sayı. Ama bir tuzaktırlar. 1710499200
saniye mi yoksa milisaniye mi? (İkisi de yaygındır. JavaScript'in Date.now()'u milisaniye verir,
POSIX saniye verir.) Hangi saat dilimi? Dönüştürücü olmadan loglarda okunamazlar. 1970'ten önceki tarihleri
temiz bir şekilde temsil edemezler. Ve henüz geçiş yapmamış sistemlerde 2038'de 32-bit tam sayıları
taşıracaklar. ISO 8601
string'leri tüm bunları çözer. UTC kullan ve her zaman saat dilimi offset'ini dahil et — sondaki bir
Z veya +00:00 olmadan çıplak 2026-03-15T11:42:00 belirsizdir ve
eninde sonunda yerel saati varsayan bir client'ta bir bug'a yol açar.
// ✅ Good — unambiguous, human-readable, timezone-explicit
{
"created_at": "2026-03-15T11:42:00Z",
"updated_at": "2026-04-01T08:15:33Z",
"scheduled_delivery": "2026-03-18T00:00:00Z",
"expires_at": "2026-04-15T23:59:59Z"
}
// ❌ Avoid — ambiguous, unreadable, seconds vs ms confusion
{
"created_at": 1710499200,
"updated_at": 1743494133000,
"scheduled_delivery": "15/03/2026",
"expires_at": "April 15, 2026"
}Null vs Atlanan Alanlar
Bu ikisi aynı değildir ve onları karıştırmak sadece edge case'lerde ortaya çıkan ince bug'lar yaratır.
Null, alanın var olduğu, sunucunun bundan haberdar olduğu ve şu anki değerinin "hiçbir şey"
olduğu anlamına gelir — henüz sevk edilmemiş bir siparişteki fulfilled_at timestamp'i gibi.
Bir alanı tamamen atlamak, bu bağlamda uygulanmadığı anlamına gelir — iade edilmemiş bir
siparişteki return_tracking_number gibi. Eğer bir client "fulfilled_at": null
görürse, alanın bu kaynağın şemasının parçası olduğunu ve açıkça ayarlanmadığını bilir. Alan yoksa, client
bunu bu cevabın kapsamı dışında görmeli — ki bu PATCH ile kısmi güncelleme yaptığında önemlidir.
null göndermek "bu alanı temizle" demektir; onu atlamak "ona dokunma" demektir.
// Order that exists but hasn't shipped yet
// fulfilled_at: null — we know about this field, it's just not set yet
// return_tracking_number: omitted — returns don't apply to this order
{
"id": "ord_9kZ2m",
"status": "processing",
"created_at": "2026-03-15T11:42:00Z",
"fulfilled_at": null,
"shipped_at": null,
"tracking_number": null,
"total_amount": 149.99
}
// PATCH /v1/orders/ord_9kZ2m — cancel the scheduled delivery
// Only include fields you want to change
{
"scheduled_delivery": null,
"status": "cancelled"
}
// "total_amount" is omitted — we're NOT zeroing it out, just not touching itPaginasyon — Cursor, Offset'ten İyidir
Offset paginasyon (?page=3&per_page=20) uygulaması sezgiseldir ve açıklaması kolaydır,
ancak canlı veride sessizce bozulur. Bir client paginasyon yaparken — sayfa 2 ile sayfa 3 arasında — bir
kayıt eklenirse, bir item'ı atlarlar. Bir kayıt silinirse, bir çift görürler. Sık değişen herhangi bir
dataset için (siparişler, olaylar, bildirimler) cursor tabanlı paginasyon doğru varsayılandır. Client'a
result set'teki konumunu temsil eden opak bir cursor (genellikle base64-kodlu bir ID veya timestamp)
verirsin. Sonraki sayfa tam o noktadan başlar, insert'lerden veya delete'lerden bağımsız olarak. Offset
paginasyon, dataset'in stabil olduğu ve kullanıcıların gerçekten sayfa 47'ye atlamaları gereken admin UI'lar
için uygundur. Infinite scroll yapan herhangi bir mobil client için uygun değildir.
// GET /v1/orders?limit=20&cursor=eyJpZCI6Im9yZF85a1oybSJ9
{
"orders": [
{ "id": "ord_9kZ2m", "status": "fulfilled", "total_amount": 149.99, "created_at": "2026-03-15T11:42:00Z" },
{ "id": "ord_8jY1l", "status": "processing", "total_amount": 89.00, "created_at": "2026-03-14T09:10:00Z" }
],
"pagination": {
"next_cursor": "eyJpZCI6Im9yZF84alk1bCJ9",
"has_more": true,
"limit": 20
}
}
// When has_more is false, omit next_cursor entirely (or set to null)
// Clients: fetch next page with ?cursor=<next_cursor> until has_more === falseAlan İsimlendirme — snake_case vs camelCase
Bir kuralı seç ve bir linter ile zorla. Asıl seçim tutarlılıktan daha az önemlidir. Bununla birlikte:
birincil tüketicilerin JavaScript/TypeScript client'ları ise, camelCase destructuring ve object
spread ile temiz bir şekilde entegre olur. Birincil tüketicilerin Python veya Ruby backend'leri ise,
snake_case doğal hissettirir. Her ikisine de hizmet ediyorsan, pragmatik çözüm kuralı
belgelemek ve client'ların bir dönüşüm katmanı kullanmasına izin vermektir — bir reviver ile
JSON.parse,
bir Python humps kütüphanesi veya framework'ündeki tek bir serileştirme config'i. Asla
yapmaman gereken şey, aynı API'de kuralları karıştırmaktır — order_total'ın yanındaki
customerId, farklı mühendislerin birbiriyle konuşmadan farklı endpoint'ler yazdığının bir
işaretidir. Endpoint'ler arasında alan isimlerini tutarlı bir şekilde belgelemek için
JSON Schema Generator'ı kullan.
Versiyonlama
İki ekol: URL versiyonlama (/v1/orders, /v2/orders) ve header versiyonlama
(Accept: application/vnd.example.v2+json veya özel bir API-Version: 2026-03-15
header'ı). URL versiyonlama pratikte neredeyse her zaman kazanır. Header'ları parse etmeden loglarda
görünürdür, hiçbir yapılandırma olmadan her HTTP client'ta çalışır, bir tarayıcıda test edebilirsin ve basit
bir path-prefix kuralıyla aynı gateway'de v1 ve v2'yi yan yana çalıştırabilirsin. Header versiyonlama
IANA medya tipi
modeli başına teorik olarak daha RESTful'dur, ancak görünmez karmaşıklık yaratır — URL'de aynı görünen bir
istek, çoğu geliştiricinin ilk kontrol etmediği bir header'a bağlı olarak aslında farklı davranır.
Stripe'ın tarih tabanlı versiyonlaması (Stripe-Version: 2024-06-20) büyük platformlar için iki
dünyanın en iyisidir, ancak bu ilk versiyonlama şemanı seçmekten farklı bir sorundur. Ne seçersen seç,
birinci günden versiyonla. Production'da versiyonlanmamış bir API'ye versiyonlama retrofit etmek acı
vericidir ve nadiren temiz geçer. Migrasyon testi sırasında her iki API sürümünden gelen cevapların
yapısal olarak sağlam olduğunu doğrulamak için JSON Validator'ı kullan.
Toparlayalım
Bunların hiçbiri çığır açıcı değil — ama asıl mesele bu. API tasarımıyla en çok mücadele eden takımlar, teknik olarak yanlış seçimler yapanlar değildir. Farklı endpoint'lerde farklı seçimler yapan ve bunları asla yazmayan takımlardır. Düz success cevapları. RFC 7807 hata body'leri. ISO 8601 tarihler. Canlı veride cursor paginasyon. "Bilinen ve boş" için null, "uygulanmıyor" için atlanan. Birinci günden URL versiyonlama. Bu pattern'ler mükemmel değil ama öngörülebilir — ve öngörülebilirlik, bir API'yi tersine mühendislik yapılacak bir bulmaca yerine entegre edilmesi zevk veren bir şey yapan şeydir. Resmi JSON spesifikasyonu RFC 8259'da yaşar, eğer bir spec seviyesi tartışmayı kapatmak gerekirse. Bu katmanın üzerindeki her şey için, en iyi standart takımının gerçekten yazdığı ve tutarlı bir şekilde takip ettiğidir.