İçeriğe geç
Muhammet Şafak
en

API yanıtlarını standartlaştırmak: tutarlı bir sözleşme

Her uçta aynı yanıt yapısını döndürmek istemci kodunu nasıl sadeleştirir ve hataları nasıl öngörülebilir kılar?

Kapak görseli — koyu duvarda üç metal kartuş: status success ile data { ... }, altında code 404 Not Found ve code 500 Internal Server Error

Bu yazı 6 yıl önce yazıldı — o günün koşullarını yansıtır.


Bir API üzerinde birden fazla istemci çalıştığında — iOS uygulaması, React SPA, başka bir servis — her uçtan farklı bir yanıt yapısı geldiğinde ne olur? Her istemci kendi ayrıştırma mantığını yazar, her hata formatı için ayrı bir if bloğu eklenir, bir uçta data anahtarı varken diğerinde result gelir. Zamanla bu tutarsızlık, istemci kodunun üzerine binen gizli bir borç haline gelir.

Bu sorunu son bir yılda birkaç projede bizzat yaşadım. Çözüm teknoloji değişikliği gerektirmiyor: sözleşmeyi API’nin dışına değil, içine yazmak yeterli.

Tutarlı bir yanıt yapısının anatomisi

İyi bir API yanıt sözleşmesi üç şeyi garanti eder: başarı mı başarısızlık mı olduğunu, taşınan veriyi ve varsa hata bilgisini. Bunlara karşılık gelen basit bir iskelet:

{
  "success": true,
  "data": {},
  "message": null,
  "errors": null
}

Başarısız yanıtta success: false, data: null, message ile kısa açıklama ve errors ile alan bazlı hatalar gelir. İstemci her yanıtta yalnızca success anahtarına bakmak zorunda; geri kalanı tutarlıdır.

PHP’de bir yanıt wrapper sınıfı

Bu yapıyı her controller’a elle yazmak yerine tek bir sınıfa taşıdım:

<?php

class ApiResponse
{
    public static function success($data = null, string $message = null, int $status = 200): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'success' => true,
            'data'    => $data,
            'message' => $message,
            'errors'  => null,
        ], $status);
    }

    public static function error(string $message, array $errors = [], int $status = 422): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'success' => false,
            'data'    => null,
            'message' => $message,
            'errors'  => $errors ?: null,
        ], $status);
    }
}

Controller’da kullanımı doğrudan:

public function store(Request $request)
{
    $validated = $request->validate([
        'name'  => 'required|string|max:255',
        'email' => 'required|email|unique:users',
    ]);

    $user = User::create($validated);

    return ApiResponse::success($user, 'Kullanıcı oluşturuldu.', 201);
}

Doğrulama hatası oluştuğunda Laravel’in kendi ValidationException’ını yakalamak ve aynı sözleşmeye döndürmek gerekiyor. Bunun için Handler.php’yi özelleştirmek yeterli — tek bir yer, tüm API hatalarını kapsar.

HTTP durum kodu ile success bayrağının ilişkisi

Bazı ekiplerde “zaten 2xx/4xx var, success bayrağına gerek yok” tartışması çıkar. Deneyimime göre bu doğru değil: 207 (Multi-Status), 422, 409 gibi kodlar istemci kütüphanelerine göre farklı yorumlanır; success bayrağı bu belirsizliği ortadan kaldırır. Öte yandan HTTP durum kodlarını görmezden gelmek de hata: 200 ile hata döndürmek istemci tarafında cache ve loglama sorunlarına yol açar. İkisi birlikte tutarlı olmalı.

Sayfalama ve liste yanıtları

Liste döndüren uçlarda veriyi düz dizi olarak değil, sayfalama meta bilgisiyle sarmak daha iyi bir pratik:

{
  "success": true,
  "data": {
    "items": [],
    "meta": {
      "current_page": 1,
      "per_page": 15,
      "total": 243,
      "last_page": 17
    }
  },
  "message": null,
  "errors": null
}

Laravel’in LengthAwarePaginator’ı bu meta bilgiyi zaten üretiyor; tek yapılacak iş onu data.meta altına taşımak.

Sözleşmeyi ekibe taşımak: geriye dönük uyumluluk meselesi

Sözleşmeyi projenin başında tanımlamanın en büyük faydası, ilerde tartışma çıkmaması. Ama sözleşmeyi mevcut bir API’ye sonradan uygulamak farklı bir iş: çalışan istemciler var, bekledikleri yapı var. Bu noktada iki seçenek kalıyor: versiyon atlamak (/v2/) ya da yalnızca yeni uçlara standart sözleşmeyi uygulamak.

Ben ikinci yolu seçtim. Mevcut uçlara dokunmadım; yeni eklediklerimde sözleşmeyi uyguladım. İstemci tarafında adaptör katmanı yazarak ikisini soyutladım. Bu geçici bir yük ama mevcut istemciyi kırmaktan daha az riskli.

Sözleşmeyi belgelemek

Sözleşme ne kadar sağlam olursa olsun, belgelenmezse ikinci geliştirici bunu bilemez. OpenAPI (Swagger) şeması yazmak bu noktada büyük fark yaratıyor; yanıt yapısı bir kez tanımlanıyor, tüm uçlar buna referans veriyor. Henüz tam bir OpenAPI entegrasyonum olmasa da en azından README.md’de üç örnek yanıt tutuyorum.


Tutarlı bir yanıt sözleşmesi sihirli bir çözüm değil, ancak getirisi maliyetinin çok üzerinde. İstemci kodu sadeleşiyor, hata senaryoları öngörülebilir hale geliyor, yeni bir uç eklendiğinde yapı tartışması yaşanmıyor. Projenin başında bu kararı vermek, ilerlediğinde geri dönüp düzeltmeye çalışmaktan çok daha kolay.

Bu konuda sorulanlar

Bu konudaki deneyler

Kaydırmalı doğru/yanlış bilgi yarışması; asıl mesele oyun değil, istemcinin ürettiği skoru imzayla doğrulanabilir kılmak.

Şu an ne yapıyor

Kaydırmalı doğru/yanlış bilgi yarışması bir mobil uygulama olarak çekirdek döngüsüyle uçtan uca çalışıyor; skoru istemci üretse de server_nonce ve HMAC imzasıyla sunucu tarafında doğrulanıyor. swipenor.com uygulamanın tanıtım sayfasıdır, oyunun kendisi değil.

Mobil uygulama Laravel PHP PostgreSQL +6 daha
Temmuz 2026 — Devam ediyor
Etiketler: #API
Paylaş:

Yorumlar

Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi