# 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?

- Yayın: 2020-01-05
- Kategori: Web Geliştirme
- Etiketler: API
- Okuma süresi: 3 dk okuma
- Kaynak: https://www.muhammetsafak.com.tr/blog/api-yanitlarini-standartlastirmak-tutarli-bir-sozlesme/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
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:

```json
{
  "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
<?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:

```php
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ı](/blog/laravel-ile-form-dogrulama-validation-pratigi) 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:

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

Laravel'in [`LengthAwarePaginator`](/blog/rest-api-de-sayfalama-filtreleme-ve-siralama)'ı 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.
