İçeriğe geç
Muhammet Şafak
Web Geliştirme 3 dk okuma

API hata sözleşmesi: istemciye anlamlı hata döndürmek

API hata yanıtlarını tutarlı bir sözleşmeye bağlamak, istemci geliştirmeyi ve hata ayıklamayı nasıl kolaylaştırır.


API tasarımında başarılı yanıtlara harcanan özen, hata yanıtlarına çoğunlukla yansımıyor. İstemci bir hata aldığında ne döneceğini bilmiyorsa, ya her şeyi genel bir hata mesajıyla yutmak zorunda kalıyor ya da her uç noktanın hatalarını ayrı ayrı öğreniyor. Bu ikisi de iyi bir sözleşme değil.

Hatayı yanıt tasarımının ayrılmaz bir parçası olarak düşünmek gerekiyor. Başarılı yanıtın şeması varsa, hata yanıtının da şeması olmalı.

HTTP durum kodları yeterli değil

HTTP durum kodları önemli ama tek başlarına yetersiz. 422 Unprocessable Entity isteğin doğrulama hatası aldığını söylüyor; hangi alan hatalı, neden hatalı — bunu söylemiyor. 500 Internal Server Error sunucunun hata verdiğini söylüyor; ne hata, nasıl bir hata — söylemiyor.

İstemciyi geliştiren kişi (siz veya bir başkası) o durum kodunu yorumlamak için ek bilgiye ihtiyaç duyuyor. O bilgiyi yanıt gövdesinde standart bir biçimde vermek sözleşmenin işi.

Tutarlı bir hata yapısı

Birkaç projede denediğim ve üzerinde karar kıldığım bir yapı şu:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Gönderilen veriler doğrulanamadı.",
    "details": [
      {
        "field": "email",
        "message": "Geçerli bir e-posta adresi giriniz."
      },
      {
        "field": "phone",
        "message": "Telefon numarası zorunludur."
      }
    ]
  }
}

Bu yapıda üç katman var:

  • code: Machine-readable sabit bir hata kodu. İstemci bu koda göre branch açabilir.
  • message: Human-readable kısa açıklama. Hata ayıklama için.
  • details: Varsa ek bağlam. Doğrulama hatalarında alan bazında mesajlar; diğer hatalarda boş dizi ya da alan yok.

Laravel’de uygulamak

Laravel’de tüm hata yanıtlarını tek noktadan yönetmek için app/Exceptions/Handler.php içindeki render metodunu kullanıyorum:

<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;

class Handler extends ExceptionHandler
{
    public function render($request, Throwable $e)
    {
        if ($request->expectsJson()) {
            return $this->handleApiException($request, $e);
        }

        return parent::render($request, $e);
    }

    private function handleApiException($request, Throwable $e)
    {
        if ($e instanceof ValidationException) {
            $details = collect($e->errors())
                ->flatMap(fn ($messages, $field) =>
                    collect($messages)->map(fn ($msg) => [
                        'field'   => $field,
                        'message' => $msg,
                    ])
                )
                ->values()
                ->all();

            return response()->json([
                'error' => [
                    'code'    => 'VALIDATION_FAILED',
                    'message' => 'Gönderilen veriler doğrulanamadı.',
                    'details' => $details,
                ],
            ], 422);
        }

        if ($e instanceof AuthenticationException) {
            return response()->json([
                'error' => [
                    'code'    => 'UNAUTHENTICATED',
                    'message' => 'Bu işlem için kimlik doğrulama gereklidir.',
                ],
            ], 401);
        }

        if ($e instanceof HttpException) {
            return response()->json([
                'error' => [
                    'code'    => 'HTTP_ERROR',
                    'message' => $e->getMessage() ?: 'İstek işlenemedi.',
                ],
            ], $e->getStatusCode());
        }

        return response()->json([
            'error' => [
                'code'    => 'SERVER_ERROR',
                'message' => 'Beklenmeyen bir hata oluştu.',
            ],
        ], 500);
    }
}

Tek bir metodun tüm exception’ları karşılaması, yeni bir hata türü eklediğinizde tek bir yere bakmanızı sağlıyor.

Hata kodları için bir kural

Hata kodlarını büyük harf ve altçizgi ile tutuyorum: VALIDATION_FAILED, RESOURCE_NOT_FOUND, QUOTA_EXCEEDED. Bu kodlar istemci tarafında switch-case veya map ile işlenebilir hale geliyor. İstemci ekibi bu kodlara göre kullanıcıya farklı mesajlar gösterebilir, farklı yönlendirmeler yapabilir.

İnsan tarafından okunabilen message alanına güvenmek istemezsiniz — bu metin değişebilir, çevrilebilir. code sabittir ve sözleşmenin bir parçasıdır.

Versiyonlama ve geriye dönük uyumluluk

Hata yapısını bir kez belirleyip sonradan değiştirmek, başarılı yanıt yapısını değiştirmek kadar kırıcı olabiliyor. İstemci error.code değerini okuyarak kendi iş mantığını yönetiyorsa, o kodun adını değiştirmek istemci tarafında sessiz bir kırılmaya yol açıyor. Bu yüzden hata kodlarını sanki bir API sözleşmesinin birinci sınıf vatandaşıymış gibi ele almak gerekiyor: yeni kod eklenebilir, eski kaldırılmamalı.

Mevcut bir kodu kullanımdan kaldırmanız gerektiğinde geçiş süresi bırakmak ve bunu dokümana yansıtmak iyi bir pratik. Başarılı yanıtları versiyonlarken hata yapısını atlamak kolay bir hata.

Ne zaman fazla karmaşıklık

Küçük bir dahili servis yazıyorsanız ve tek bir istemci var, o da sizseniz, bu yapıya gerek yok. Durum kodu + kısa mesaj yeterli olabilir.

Ama birden fazla istemciniz varsa (web, mobil, harici bir ortak), ya da API’yi dışa açıyorsanız, bu sözleşme istemci geliştirme sürecini gerçekten kolaylaştırıyor. “400 aldım, ne anlama geliyor” sorusuyla harcanan zaman birikerek önemli bir maliyet oluşturuyor.

Hata yanıtları, API’nizin sözleşmesinin bir parçasıdır. Başarılı yanıtlar kadar özenle tasarlanmayı hak ediyor.

Etiketler: #API
Paylaş:

İlgili Yazılar

Sitede Ara

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

Esc ile kapat Pagefind ile güçlendirildi