sazak@labs:~
oturum

etiketler

IdempotencyRESTHTTPbackward compatibilityAPIAPI design

Software · Feb 1, 2024 · 10 dk okuma

API Nedir? İyi Bir API Tasarımı Neden Endpoint Yazmaktan Fazlasıdır?

API, iki yazılım arasındaki iletişimin contract'ıdır. İyi bir API tasarımı resource modelinden HTTP methods ve error contract'a, idempotency'den backward compatibility'ye kadar birçok kararı birlikte gerektirir.

API Nedir? İyi Bir API Tasarımı Neden Endpoint Yazmaktan Fazlasıdır?

API tasarlamak, bir uygulamanın dışarıya açtığı endpoint'leri belirlemekten ibaret değildir. API, iki yazılım bileşeninin hangi işlemleri hangi koşullarda gerçekleştirebileceğini tanımlayan bir contract'tır. Bu contract; veri yapısını, işlemlerin anlamını, hata davranışını ve zaman içinde hangi garantilerin korunacağını kapsar. Endpoint yalnızca bu anlaşmanın erişilebilir adresidir.

Bu ayrım önemlidir çünkü kötü API kararlarının maliyeti çoğu zaman API ilk yayınlandığında ortaya çıkmaz. Başka bir ekip entegrasyonu tamamladığında, mobil uygulama eski sürümle çalışmaya devam ettiğinde veya bir ödeme isteği network timeout nedeniyle tekrar gönderildiğinde görünür. İyi tasarımın amacı her ihtimali önceden tahmin etmek değil, istemcilerin hangi davranışlara güvenebileceğini açıkça belirlemektir.

API, endpoint listesinden önce bir contract'tır

API, Application Programming Interface ifadesinin kısaltmasıdır. Bir yazılımın başka bir yazılıma sunduğu programatik arayüzü ifade eder. HTTP üzerinden çalışan bir web API'sinde bu arayüz genellikle URL, HTTP method, request body, response body ve status code üzerinden kullanılır. Ancak API kavramı HTTP ile sınırlı değildir; bir kütüphanenin public fonksiyonları veya işletim sisteminin sunduğu arayüzler de API'dir.

Web API tasarımında contract, istemcinin güvenebileceği davranışların toplamıdır. Örneğin GET /orders/42 isteğinin bir siparişi döndürmesi yeterli bir tanım değildir. Sipariş bulunamadığında ne olacağı, hangi alanların döneceği, tarihlerin nasıl gösterileceği ve istemcinin hangi yetkiye ihtiyaç duyacağı da contract'ın parçasıdır.

Bir API'nin teknik olarak çalışması, contract'ının açık olduğu anlamına gelmez. Sunucu geçerli JSON döndürebilir fakat status alanının hangi değerleri alabildiği belirsizse istemci güvenilir karar veremez. Benzer şekilde, belgelerde başarılı response gösterilip hata durumları açıklanmamışsa entegrasyonun önemli bir bölümü istemci geliştiricisinin tahminlerine bırakılmış olur.

OpenAPI gibi bir schema tanımlama aracı burada faydalıdır. Endpoint'leri, parametreleri, request ve response yapılarını makine tarafından okunabilir biçimde belgeleyebilir. Aynı tanım üzerinden documentation, client code ve contract test üretilebilir. Fakat OpenAPI dosyasının varlığı tek başına iyi tasarım anlamına gelmez; dosya yalnızca içine yazılmış kararlar kadar doğrudur.

Contract'ın bir kısmı statik, bir kısmı davranışsaldır. Bir alanın string olması statik bilgidir. Aynı idempotency key ile yinelenen ödeme isteğinin ikinci bir ödeme oluşturmaması ise davranışsal garantidir. İkinci tür kurallar yalnızca schema doğrulamasıyla güvence altına alınamaz; uygulama mantığı ve testlerle korunmalıdır.

Resource modeli, HTTP'nin anlamını belirler

REST API tasarımında başlangıç noktası endpoint isimleri değil, resource modelidir. Resource, sistemin dışarıya kimliği ve anlamı olan bir varlık olarak sunduğu şeydir. Sipariş, müşteri, ödeme veya teslimat birer resource olabilir. Bunlar veritabanı tablolarıyla birebir eşleşmek zorunda değildir.

Örneğin /orders sipariş koleksiyonunu, /orders/42 belirli bir siparişi temsil edebilir. Buradaki önemli karar URL'nin çoğul yazılması değil, siparişin hangi yaşam döngüsüne sahip olduğudur. Bir sipariş oluşturulabilir, görüntülenebilir veya iptal edilebilir; ancak her siparişin her alanı serbestçe değiştirilebilir olmayabilir.

HTTP methods bu işlemlerin anlamını taşır. GET resource'u okumak, POST çoğunlukla yeni resource oluşturmak veya bir işlemi başlatmak, PUT hedef resource'un temsilini değiştirmek, PATCH kısmi değişiklik yapmak ve DELETE resource'u kaldırmak için kullanılır. Bu ayrımlar yalnızca isimlendirme tercihi değildir; HTTP'nin standart davranışlarıyla ilişkilidir.

GET güvenli, yani safe bir method'dur: istemci bu isteği yaptığı için resource üzerinde istenen bir durum değişikliği oluşmamalıdır. Sunucu erişim log'u tutabilir veya sayaç güncelleyebilir; fakat GET /orders/42/cancel gibi sipariş iptal eden bir endpoint HTTP'nin güvenli okuma beklentisini bozar. Browser, crawler ve cache gibi bileşenler GET isteklerini farklı koşullarda gerçekleştirebilir.

PUT ve DELETE idempotent method'lardır. Idempotency, aynı isteğin birden fazla kez uygulanmasının hedeflenen sunucu durumu üzerindeki etkisinin bir kez uygulanmasıyla aynı olmasıdır. Örneğin aynı resource'u iki kez silmek, ilk silme başarılı olduktan sonra onu yeniden silinmiş hâle getirmez. İkinci isteğin status code'u farklı olabilir; idempotency, response'ların birebir aynı olması demek değildir.

POST varsayılan olarak idempotent değildir. Aynı POST /orders isteği iki kez gönderildiğinde iki farklı sipariş oluşabilir. PATCH de genel olarak idempotent kabul edilmez; ancak belirli bir PATCH işlemi idempotent tasarlanabilir. status alanını cancelled değerine ayarlamak ile miktarı her istekte bir artırmak arasındaki fark budur.

HTTP status codes contract'ın bir başka parçasıdır. 200 OK başarılı response'u, 201 Created resource oluşturulmasını, 204 No Content başarılı fakat body içermeyen sonucu ifade eder. 400 Bad Request genel istemci hatalarında, 401 Unauthorized geçerli authentication bulunmadığında, 403 Forbidden erişim yetkisi olmadığında ve 404 Not Found hedef bulunamadığında kullanılır.

409 Conflict, mevcut resource durumuyla çelişen işlemler için anlamlıdır. 422 Unprocessable Content ise sunucunun içerik türünü anlayıp request'i sözdizimsel olarak işleyebilmesine rağmen içerdiği talimatları gerçekleştiremediği durumlarda kullanılabilir. Validation hataları için 400 veya 422 seçilebilir; kritik olan bu ayrımın API boyunca tutarlı uygulanmasıdır. Beklenmeyen sunucu hataları ise uygun 5xx kodlarıyla temsil edilmelidir.

Asıl sorun JSON taşımak değil, belirsiz davranıştır

API'ler sıkça iki sistemin JSON üzerinden konuşmasını sağlayan araçlar olarak açıklanır. Bu açıklama başlangıç için kullanışlıdır ama kritik mekanizmayı kaçırır: client ve server birbirinden bağımsız çalışırken ortak bir davranış modeline ihtiyaç duyar. JSON yalnızca verinin gösterim biçimidir. JSON'un geçerli olması, işlemin güvenilir veya doğru olduğu anlamına gelmez.

Bunun en belirgin örneği network timeout durumudur. İstemci POST /payments isteği gönderir, sunucu ödemeyi kaydeder fakat response istemciye ulaşmaz. İstemci açısından iki olasılık vardır: ödeme gerçekleşmemiştir veya ödeme gerçekleşmiş ama yanıt kaybolmuştur. Timeout tek başına bu iki durumu ayıramaz.

İstemci aynı isteği tekrar gönderirse ikinci bir ödeme oluşturulabilir. Tekrar göndermekten vazgeçerse gerçekten başarısız olmuş bir ödeme tamamlanamayabilir. Sorun endpoint'in çalışmaması değil, dağıtık sistemde işlemin sonucunun belirsiz kalmasıdır.

Bu tür işlemler için uygulama seviyesinde idempotency key kullanılabilir. İstemci mantıksal işlem başına benzersiz bir anahtar üretir ve örneğin Idempotency-Key header'ıyla gönderir. Sunucu aynı anahtarla gelen uyumlu tekrarları yeni işlem olarak yürütmek yerine önceki işlemin sonucuyla ilişkilendirir.

Bu mekanizma yalnızca bir header eklemekten ibaret değildir. Sunucu anahtarı belirli bir kapsamda, örneğin müşteri hesabı ve endpoint ile birlikte saklamalıdır. Aynı anahtarın farklı request body ile kullanılması reddedilmeli veya açıkça tanımlanmış bir conflict davranışı üretmelidir. Aynı anda gelen iki isteğin ikisinin de işlemi başlatmasını önlemek için kayıt ve işlem koordinasyonu atomik olmalıdır.

Idempotency key kayıtlarının ne kadar süre tutulduğu da contract'ın parçasıdır. Kayıt süresi dolduktan sonra aynı anahtarın yeni işlem sayılabileceği açık olmalıdır. Key mekanizması, ödeme sağlayıcısıyla yapılan işlemin veya database transaction'ın kendi tutarlılık sorunlarını otomatik olarak çözmez.

Burada önemli bir sınır vardır: Tam olarak bir kez gerçekleşen işlem garantisi, yalnızca HTTP isteğini tekrar deneyerek elde edilmez. Daha doğru hedef, tekrarların kaçınılmaz olabileceğini kabul edip aynı mantıksal işlemin istenmeyen ikinci bir iş etkisi üretmesini engellemektir. Bunun için database unique constraint, transaction, durum kontrolü ve gerektiğinde dış sistemlerle reconciliation birlikte kullanılabilir.

Aynı belirsizlik hata tasarımında da ortaya çıkar. Her başarısız işlem için 500 döndüren veya her response'u 200 yapıp body içinde success: false taşıyan API, HTTP'nin yerleşik anlamlarından yararlanmaz. İstemci hangi hatanın düzeltilebilir, hangisinin tekrar denenebilir olduğunu kendisi tahmin etmek zorunda kalır.

İyi bir error contract en azından makine tarafından yorumlanabilir bir error code, insan tarafından okunabilir açıklama ve request ile ilişkilendirilebilen bir identifier sağlamalıdır. Validation hatalarında alan bazlı ayrıntılar da gerekir. Örneğin INVALID_QUANTITY kodu ile items[0].quantity alanının sıfırdan büyük olması gerektiğinin belirtilmesi, genel bir 'Invalid request' mesajından çok daha işlevseldir.

HTTP status code hatanın genel sınıfını, uygulamaya özgü error code ise istemcinin uygulayacağı kararı belirtir. Error mesajının metni değişebilir; istemcilerin kontrol akışını bu metni karşılaştırarak kurması kırılgandır. Sabit error code'lar ve belgelenmiş davranışlar tercih edilmelidir.

Bir sipariş API'sinde kararlar birbirine bağlanır

Bir backend developer'ın sipariş oluşturma endpoint'i hazırladığını düşünelim. Önündeki gerçek çalışma malzemesi yalnızca controller dosyası değildir: OpenAPI contract, request validation kuralları, sipariş tablosu, transaction sınırları ve mobil uygulamanın sipariş ekranı birlikte değerlendirilmelidir. İstek örneği POST /orders ve body olarak customerId, items ve deliveryAddress alanlarını içeriyor olsun.

İlk karar resource'un kimliği ve sahipliğidir. Authentication uygulanıyorsa customerId değerinin istemciden serbestçe kabul edilmesi, başka bir müşteri adına sipariş oluşturma riskini doğurabilir. Sunucu müşteri kimliğini doğrulanmış oturumdan türetebilir veya request'teki değerin ilgili hesap için yetkili olduğunu kontrol edebilir. Validation yalnızca alanların doğru türde olup olmadığı değildir; işlem yapmaya yetkili olunup olunmadığı ayrı bir authorization kontrolüdür.

Ardından business validation gelir. items boş olmamalı, quantity geçerli aralıkta bulunmalı ve ürünler sipariş oluşturulurken satılabilir durumda olmalıdır. Fiyatın istemciden gelen değere güvenilerek hesaplanması yerine sunucunun yetkili fiyat kaynağından belirlenmesi gerekir. JSON schema, negatif miktarı engelleyebilir; fakat stok uygunluğunu veya kampanya koşullarını tek başına doğrulayamaz.

Başarılı oluşturma işleminde 201 Created dönmek ve yeni resource'un adresini Location: /orders/42 ile bildirmek uygun bir tercihtir. Response içinde id, status, totalAmount, currency ve createdAt gibi alanlar bulunabilir. Para tutarının hangi birimde ve hangi hassasiyetle temsil edildiği açıklanmalıdır; aksi hâlde client tarafında yuvarlama ve gösterim hataları oluşabilir.

Siparişin durum geçişleri de contract'a dahildir. pending durumundaki bir sipariş iptal edilebilirken shipped durumundaki sipariş için aynı işlem reddedilebilir. Bu durumda POST /orders/42/cancellation gibi ayrı bir işlem resource'u tasarlanabilir veya mevcut modelin gerektirdiği başka bir yaklaşım seçilebilir. Önemli olan iptalin yalnızca bir alan güncellemesi değil, kuralları olan bir business operation olmasıdır.

Sipariş listeleme endpoint'i farklı sorunlar getirir. GET /orders ilk geliştirme sırasında bütün kayıtları döndürebilir ancak veri büyüdükçe bu yaklaşım hem sunucu maliyetini hem response boyutunu artırır. Pagination bu nedenle başlangıçtan itibaren contract'ın parçası olmalıdır.

Offset pagination için GET /orders?limit=20&offset=40 anlaşılır bir tasarımdır. Fakat yeni siparişler eklenirken offset kullanan istemci aynı kaydı tekrar görebilir veya bazı kayıtları atlayabilir. Büyük ve sürekli değişen koleksiyonlarda cursor pagination daha uygun olabilir: GET /orders?limit=20&cursor=... isteğindeki cursor, sonraki sayfanın nereden başlayacağını temsil eder.

Cursor tasarımında sıralama kararlı olmalıdır. Yalnızca createdAt ile sıralama yapmak yeterli olmayabilir çünkü iki sipariş aynı timestamp'e sahip olabilir. createdAt ile id birlikte kullanılarak deterministik sıra kurulabilir. Cursor bu sıralama anahtarlarını temsil edebilir ve istemcinin iç yapısını bilmesini gerektirmeyen opaque bir değer olarak sunulabilir.

Filtering ve sorting aynı contract'ın devamıdır. Örneğin GET /orders?status=pending&sort=-createdAt belirli durumdaki siparişleri en yeniden eskiye sıralayabilir. Ancak istemcinin herhangi bir database kolonunu filtre veya sıralama parametresi olarak kullanmasına izin vermek iyi bir varsayılan değildir. Desteklenen alanlar açıkça tanımlanmalı, geçersiz parametreler tutarlı validation hatası üretmelidir.

Pagination sırasında filtering veya sorting değişirse önceki cursor artık aynı veri kümesini temsil etmeyebilir. Bu nedenle cursor belirli query koşullarına bağlanabilir ve koşullar değiştiğinde reddedilebilir. Ayrıca erişim yetkisi olmayan siparişlerin pagination öncesinde sorgu kapsamından çıkarılması gerekir; aksi durumda sayfa boyutları veya metadata üzerinden yetkisiz veri hakkında bilgi sızabilir.

Bu örnekte contract test yalnızca POST /orders isteğinin 201 döndürdüğünü doğrulamamalıdır. Geçersiz quantity, yetkisiz customerId, yinelenen idempotency key, iptal edilemeyen sipariş durumu ve geçersiz cursor gibi durumlar da test edilmelidir. Gerçek entegrasyon hataları genellikle başarılı request örneğinin dışında kalır.

Değişiklik yönetimi tasarımın bir parçasıdır

API yayınlandıktan sonra contract artık yalnızca sunucu ekibinin kontrolünde değildir. Farklı client sürümleri aynı anda çalışabilir. Bir mobil uygulamanın güncellenmesi kullanıcı davranışına bağlıdır; bütün istemcileri aynı gün yeni contract'a geçirmek çoğu zaman mümkün değildir.

Backward compatibility, yeni API davranışının mevcut istemcilerin dayandığı eski garantileri korumasıdır. Response'a yeni ve isteğe bağlı bir alan eklemek genellikle uyumlu bir değişikliktir, ancak bütün istemcilerin bilinmeyen alanları tolere ettiği varsayılamaz. Var olan alanı kaldırmak, türünü değiştirmek veya daha önce kabul edilen bir request'i reddetmeye başlamak ise uyumluluğu bozabilir.

Daha ince kırılmalar da vardır. status alanına yeni bir enum değeri eklemek, istemcide exhaustive switch kullanan kodu bozabilir. Pagination'ın varsayılan sıralamasını değiştirmek, response schema aynı kalsa bile liste ekranındaki davranışı değiştirebilir. Error code değiştirmek de hata yönetimi mantığını etkileyebilir.

Versioning bu değişiklikleri yönetmek için bir araçtır. /v1/orders ve /v2/orders gibi URL tabanlı versioning anlaşılır ve görünürdür. Header veya media type üzerinden versioning de mümkündür. Fakat hangi yöntemin kullanıldığı, uyumluluk politikasının kendisinden daha az önemlidir.

Her alan eklemesinde yeni major version çıkarmak sürdürülebilir değildir. Buna karşılık büyük bir semantic değişikliği aynı version içinde sessizce yayınlamak da risklidir. Önce değişikliğin mevcut contract'ı bozup bozmadığı belirlenmeli, ardından gerekiyorsa yeni version veya ayrı bir resource modeli sunulmalıdır.

Deprecation kararı da uygulama planı gerektirir. Eski endpoint'in ne kadar süre destekleneceği, hangi istemcilerin hâlâ kullandığı ve geçişin nasıl ölçüleceği belirlenmelidir. Kullanım telemetry'si olmadan eski version'ı kaldırmak, görünmeyen entegrasyonları bozma riskini artırır.

Burada savunduğum tercih, dışarıya açık API'lerde açık contract, dar kapsamlı resource modeli ve değişiklik öncesi compatibility kontrolünü varsayılan çalışma biçimi yapmaktır. Yeni endpoint açmadan önce hangi client davranışının garanti edildiğini yazmak, daha sonra düzeltilemeyecek belirsizlikleri azaltır. Contract test'ler bu garantileri sürekli denetlemelidir.

Bu yaklaşımın sınırı da nettir. Tek bir uygulamanın içinde, aynı repository'de ve aynı deployment ile güncellenen iki modül arasında uzun süreli backward compatibility politikası gereksiz maliyet yaratabilir. Burada basit bir internal interface ve birlikte değiştirilen kod yeterli olabilir. Ancak bağımsız deployment, üçüncü taraf entegrasyonu veya farklı client sürümleri devreye girdiğinde contract artık uygulama ayrıntısı değil, korunması gereken bir ürün yüzeyidir.

yorumlar

0

Henüz yorum yok. İlk notu sen bırak.

Yorum yazmak için giriş yap.

Gmail ile saniyeler içinde, ya da e-posta ile kaydol.

bağlı 26109:33:00