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?
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): array
{
return response()->json([
'success' => true,
'data' => $data,
'message' => $message,
'errors' => null,
], $status);
}
public static function error(string $message, array $errors = [], int $status = 422): array
{
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.