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.

json
// ✅ 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-Typeapplication/problem+json olan hafif bir standarttır. Benimse ve hata formatın herhangi bir geliştiricinin dokümanlara uzanmadan okuyabileceği bir şey olur.

json
// 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 errors gibi 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.
  • type URI'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).

json
// 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 Location header'ı 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-After header'ı 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.

json
// ✅ 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.

json
// 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 it

Paginasyon — 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.

json
// 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 === false

Alan İ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.

Tasarım ipucu: Gerçek bir API cevabı tasarlıyorsan veya debug ediyorsan, JSON'u JSON Formatter'a yapıştır — minify edilmiş cevapları güzelce yazdırır, yapıyı vurgular ve bir client SDK'ya geçmeden önce isimlendirme tutarsızlıklarını bir bakışta fark etmene izin verir.

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.