Linux Admin

Wednesday, August 01, 2012

REST API yazarken nelere dikkat etmeli


Bir süre önce şirkette kullandığımız bir uygulama için API yazmamız gerekince REST ile yapayım diyerekten bu konuda biraz araştırma yapıp notlar almıştım, paylaşsam birilerinin işine yarayabilir belki diyerek buraya da koyuyorum.

REST in maalesef yaygin kabul görmüş bir standartı yok. Ancak REST yapısı genellikle HTTP metodlarına dayandığı için ideal bir REST servisi dendiğinde HTTP protokolünü adam gibi uygulamış olması ve genel URL ilkelerine uyulması gibi genel bir beklenti var.

HTTP: http://www.ietf.org/rfc/rfc2616 (Hypertext Transfer Protocol -- HTTP/1.1)
URI in WWW: http://www.ietf.org/rfc/rfc1630 (A Unifying Syntax for the Expression of Names and Addresses of Objects on the Network as used in the World-Wide Web)

API Versiyonu

Versiyon bilgisine sunduğunuz API'de bir şekilde yer vermeniz özellikle önemli çünkü yazdığınız API kullanılmaya başlandığında artık onu değiştiremeyeceğinizi farkedeceksiniz.

API'nizin versiyonu Path'in içinde, "Query String" olarak ya da HTTP header olarak olabilir.

Path:
GET /v1/users/bekir HTTP/1.1
Query String:
GET /users/bekir?version=1 HTTP/1.1
Version Header:
GET /user/123 HTTP/1.1
Host: api.example.com
X-API-Version: 1
Accept: application/json
Accept Header:
GET /user/123 HTTP/1.1
Host: api.example.com
Accept: application/json-v1
İşin aslı URL'in tanımına bakınca bir kaynağın tek bir URL'i olması gerekli ve bu bağlamda path'e "/v1/" eklenmesi ilgili kaynağın bir kaç farklı URL'i olması gibi geçersiz görünen yöntem oluyor ancak burada teknik anlamda ideal olan yerine daha çok pratik yöntemi seçmek bana daha akıllıca geliyor.

Versiyon bilgisini Path'in bir parçası haline getirmek şu dertleri çözüyor:
  • Birden fazla versiyonu aynı anda yükleyebiliyor ve bir diğerini etkilemeden güncelleme/hata düzeltmeleri yapabiliyorsunuz,
  • Bir versiyon için yaptığınız yükleme işlemi diğer versiyonları etkilemiyor,
  • Sunucu'da farklı yapılandırmalar ile yönetebiliyorsunuz, hatta farklı sunucuya koymanız da kolay oluyor,
  • Farklı dizinlerde kurulu ve kendi path'lerinde çalışan farklı veriyonları aynı anda kullanırken hata ayıklamak çok daha kolay oluyor, genellikle loglarını da ayırmış oluyorsunuz zaten bu yöntemle.

Kimlik doğrulama

Her istekte kullanıcı adı ve parola göndermek çok mantıklı değil, bu yöntemi tercih etmeyin:
GET http://api.example.com/v1/songs?artist=michael&username=[apiuser]&pass=[apipass]
Eğer kodunuzu bu şekilde yazarsanız şu durumlar oluşabilir:
  • Özellikle GET metodu kullandığınızda URL icinde giden bu gibi istekler sunucularda öntanımlı olarak loglanırlar ve loglarda kullanıcı adı ve parolaları görmek çok takdir alan bir davranış değildir.
  • her istekte yetkilendirme bilgileri tekrar ve açık olarak gönderilir
Yukarıdaki yontem yerine en azından HTTP Basic Auth kullanabilirsiniz ama bu senaryoda da her istekle login bilgisini göndermeniz gerekiyor. Gene de bu yöntemi tercih edebilirsiniz.

Yukarıdaki yöntemleri kullanmak yerine en azından kullaniciniza bir token dönen ayrı bir istek olusturup, login bilgisinin POST ile buraya gönderilmesini sağlayın (genellikle /auth path'i kullanılıyor.)
POST https://api.example.com/v1/auth
Host: api.example.com
...
username=[apiuser]&pass=[apipass]
Geriye geçici bir süre geçerli olacak bir token dönebilirsiniz, istemci sonraki isteklerinde bu token'i kullanarak gelebilir (Query String ya da HTTP header ile alabilirsiniz Token'i), bu sayede loglara erisimi birisi kullanıcıya ait kritik bilgileri göremez ve oturumunu ontanımlı ayarları değiştirilmemiş bir sunucu üzerinden çalamaz. Ancak bu yöntem sizi bir çok kötü durumdan koruyamaz.

Halka açık bir API geliştiriyorsanız en uygun yöntem OpenID+OAuth gibi yöntemler kullanın. Kendizinkini keşfetmeye çalışmayın, çok zaman alır ve büyük hatalar yapabilirsiniz. Konu güvenlik olunca uzman değilseniz kendize güvenmemek daha güvenilir oluyor.

Kimlik doğrulama adımları için her zaman "https" kullanın, http isteklerini izlemek fazla kolay.
Daha çok çeşitli yöntemler de mevcut ancak şimdilik bu kadarı yeterli sanirim.

Belgeleme

Yardım sayfası
Belgeleriniz için dev.example.com, developer.example.com adreslerini tercih edebilirsiniz.
Yönlendirme
API'nizin kök adresine bir tarayıcıdan ile gelen GET istelerini API'nin belgelerinin olduğu yere yönlendirmeniz kullanacak insanlara çok yardım eder.
Test sayfası
İnsanlara servisinizi tarayıcıdan test edebilecekleri bir arayüz sunarsanız çok mutlu olurlar. (Ama bu durum servisinizde sadece GET ve POST yazarsanız mümkün olabilir, aşağıda PUT, DELETE ve PATCH gibi işlevleri de kullanmanız durumunda size yardımcı olabilecek bir çakallıktan ayrıca bahsettim.)
API'nizin yardım sayfalarında şunlara yer vermeniz gerek:
  • hangi kaynaklar (Path) bulunuyor
  • farklı kaynakların hangi özellikleri mevcut,
  • en önemlisi her olası istek için (Header'ları da içeren) örnek istek ve cevap
  • eğer API'nizin versiyonları varsa (ki bence her türlü olmalı) belgelerinizi de versiyonlarsanız güzel olur
  • farklı programlama dillerinde nasıl kullanılabileceğini anlatan örnekler koyarsanız daha güzel olur

Cevap(Dönüş) biçimleri

Servisinizin cevaplarını XML, HTML, JSON gibi formatlarda dönebilirsiniz. Bunları aynı anda desteklemeniz de mümkün ancak hangi durumda hangi tip cevap göndereceğinize karar vermeniz için bir kaç yöntem mevcut.
Uzantı kullanımı: İsteğinizi gönderirken şu formatta gönderebilirsiniz:
GET /v1/users.json?username=bekir

GET /v1/users/bekir.json
Query String Kullanımı:format gibi bir anahtar kelime ile:
GET /v1/users?format=json

"Accept" HTTP Header: İsteği yaparken istediğimiz cevap formatını isteğin başlığında iletebilirsiniz:
GET /v1/users?username=bekir
Host: api.example.com
Accept: application/json
...

HTTP/1.1 200 OK
Content-Type: application/json
Her üç durumda da döndüğünüz cevabın HTTP Header'ında "Content-Type" değerini göndermek gerekli. Bir yöntemin diğerlerine üstün olup olmadığı hakkında bir yorumum yok, ucunu birlikte kullanmanızda da bir sakınca yok.

Uzantı olarak yazmak insan olarak basta daha sıcak geliyor ancak bir içeriğin tek bir URL'i olması ilkesini çiğnediği için servisinizi REST olmaktan uzaklaştırıyor.

CRUD işlemleri

REST API'lerde genellikle bir liste ifade eden koleksiyonlar ve onların içinde yer alan ögeler bulunurlar. Bu öğeler üzerinde uygulanan işlemler genellikle CRUD olarak ifade edilir ve bunları gerçeklemek için uygun HTTP metodları tercih edilir.
  • create (yarat, POST)
  • read (oku/getir, GET)
  • update (güncelle, PUT/PATCH)
  • delete (sil, DELETE)
API'nizde PUT/PATH/DELETE gibi çağrılar kullanmak istemeyebilirsiniz, bunun nedenleri genellikle şunlardır:
  • API'nizin kullanıcıları GET ve POST dışındaki HTTP metodlarından haberdar olmayabilir
  • API'nizin tarayıcılar üzerinden de kolaylıkla çalışmasını ver test edilebilmesini isteyebilirsiniz
Eğer siz de benim gibi tarayıcılardan test edilebilen bir API istiyorsanız HTTP "method override" yönteminden faydalanabilir ya da REST uyumluluğunuzu biraz daha kırarak URL'lerinize eylemler ekleyebilirsiniz.

Method Override

PUT/POST/DELETE yerine POST kullanabilir ve Query string ile isteğinizin bu 3 tarayıcı dışı çağrı gibi yorumlanmasını sağlayabilirsiniz.

Bu yöntem normal çalışma yapınızı kırmaya zorlamadan tarayıcılar üzerinde de çalışan test sayfaları oluşturacaksanız işinize yarar ve istekleri ele alan katmanın önüne ekleyeceğiniz basit bir kod ile
POST /users/bekir?method=put

POST /users/bekir?method=delete
Ya da benzer şekilde HTTP header'larından faydalanabilirsiniz:
POST /users/bekir HTTP/1.1
Host: api.example.com
X-HTTP-Method-Override: DELETE
...

Eylemin path uzerinde verilmesi

CRUD işlemlerini path'e gömebilirsiniz, ancak REST uyumlu bir API geliştiriyorsanız bu yöntem hatalı kullanım olarak yorumlanır. GEnel pratik olarak URL'lerde eylemlere yer verilmemelidir.
create:
POST /users/bekir/create
update:
POST /users/bekir/update
delete:
POST /users/bekir/delete
Bu yöntemin en büyük olumsuz yönü yapıya hakim olmayan insanların yeni eylemler eklemeye çalışması. Yeni gelen bir kişi farkında bile olmadan yeni bir eylem ekleyiverir ve mevcut CRUD yapınız daha siz farkına biel varmadan kırılır:
POST /users/bekir/move

Koleksiyonlar ve Öğeler

Koleksiyonlar için çoğul isim tercih edin, bu sayede tek bir kayıt dönen bir URL olmadığı daha anlaşılır oluyor ve kullanan kişiye yardımcı oluyor.

create

Yeni öğe yaratmak için:
POST /v1/users/bekir

HTTP/1.1 201 Created
Yeni kolksiyon oluşturmak genellikle sık karşılaşılmayan bir durum ancak gene de gerekli ise öğe yaratıeken kullandiginiz yöntemleri izleyebilirsiniz.

read

Bir koleksiyon içindeki ögeleri sorgulamak için:
GET /v1/users
Ya da bir öğeyi edinmek için:
GET /v1/users/bekir
koleksiyon hakkında
Eger kullandığınız koleksiyona dair bilgileralmak isterseniz /info gibi bir path eklentisi kullanabilirsiniz:
GET /v1/users/info
koleksiyonlarda sayfalama
Çok fazla ya da sınırsız sayıda öğenin bulunduğu durumlar için sayfalama yapmak gerekebilir, bu durumda şöyle yapılar kullanabilirsiniz:
GET /v1/users?offset=200&count=100

GET /v1/users?page=2&count=100
koleksiyon içerisinde filtreleme/arama
koleksiyonlarda filtreleme/sorgulama yapmak isterseniz "Query String"lerden faydalanabilirsiniz:
GET /v1/users?uid=1053

GET /v1/users?lastname=doğan
sonuç içinde istenen alanlar
Eğer sorgu sonucunda bütün alanları değil de sadece belli alanları istiyorsanız fields gibi bir anahtar kelime ile bunu yapabilirsiniz:
GET /v1/users?fields=displayname,team,department

update

Koleksiyonlar üzerinde güncelleme konusuna girmiyorum. Ancak öğeler üzerinde güncelleme yapmak için:
PUT /v1/users/bekir
Kısmi güncellemeler: PUT metodu genellikle tüm öğeyi güncellemek için kullanılır ancak tüm öğeyi değil de sadece belirli alanlarını güncelleyeceksek PATCH metodunu kullanabiliriz:
PATCH /v1/users/bekir

delete

Koleksiyon silmek de pek sık karşılaşılmayan bir durum ancak gerekirse bir koleksiyonu silmek için:
DELETE /v1/user/bekir/playlists/benden+size
silme işlemi genellikle koleksiyon dahilindeki öğeler için uygulanıyor:
DELETE /v1/users/bekir

Hata Durumları

HTTP durum kodlarını doğru kullanın ( http://tr.wikipedia.org/wiki/HTTP_durum_kodlar%Ç4%B1 ), hata kodları bilgisayarlar içindir ve hata durumları için uygun durum kodları kullanmazsanız API kullanıcılarınızı metin ayrıştırmaya zorlarsınız.

Hata cevabınızda genel bir şablon belirleyin ve insanların anlayabileceği de bir mesaj alanınız daima olsun. Gelen istek ne olursa olsun hata durumunda belirlediğiniz şablona uyun.

HTTP durum kodunu hata şablonunuzun içine de fazladan eklemek genellikle mantıklı bir yöntem oluyor, API kulanıcılarınız hatayı ayrıştırırken daha rahat ederler.
GET /companies/acme/customers?token=1234 HTTP/1.1
Host: api.exampla.com
...

HTTP/1.1 403 Forbidden
Content-Length: ...
...

{"status": 403, "message": "You are not auhtorized to access this content."}
Not: Yazımda sıkça kullandığımız terimleri Türkçe'ye çevirmedim. Bu şekilde yarı türkçe yarı ingilizce bir yazı okumak rahatsız edici olabiliyor ancak bu şekilde yapmasaydım anlaşılırlıktan fazlaca taviz vermem gerekecekti.

3 comments:

  1. İlk gördüğümde URL içerisinde API versiyonu kullanmak ve "?method=put" kullanımını görünce "felaket bir yazı" deyip gaza gelmiştim kendi kendime ama bunların dışında güzel bir yazı olmuş elinize sağlık.

    API versiyonunu header ya da subdomain içinde kullanmak çok daha temiz olacaktır diye düşünüyorum. "?method=" yöntemi ise sorgu parametrelerinin çok net kötüye kullanımı. Header kullanmak ya da doğrudan uygun yöntem ismini kullanmak çok daha iyi. Tüm tarayıcılar bunu destekliyor. Firefox için ek olarak https://addons.mozilla.org/en-us/firefox/addon/restclient/ adresinde bunu kolaylaştıran bir eklenti de var.

    Son olarak, kimlik doğrulama gerektiren her eylemde HTTPS kullanmayı tavsiye etmeniz çok daha iyi olur çünkü aldığınız bir "token" da HTTP kullanıldığında açık oalrak iletiliyor ve bir başkası tarafından ele geçirilmeye çok müsait(bkz. FireSheep vakası).

    ReplyDelete
  2. Sağol yorumların için, ama kimlik doğrulama kısmında zaten POST örneğindeki URL'de https kullanmış ve "Kimlik doğrulama adımları için her zaman 'https' kullanın, http isteklerini izlemek fazla kolay." diye bir ifade eklemiştim, sanırım gözünden kaçtı.

    Sorgu parametrelerine "method" ekleyerek kötüye kullanımı konusunda haklısın, yazarken de rahatsiz edici geldi ama insanları sürekli Header'lara zorlamak özellikle API kullanımına aşina olmayan insanlar için biraz ağır.

    Jstanbul'da da anlaşılıyordu zaten HTTP'yi doğru kullanmak konusunda katı olduğun :) ama diğer yandan API'lerin tüketicileri genellikle HTTP'ye hakim insanlar gibi düşünme eğiliminde olsak da bir çoğu bihaber. Bu yüzden zaman zaman varolan yöntemleri kötüye kullanmak pahasına da olsa o insanların işlerini kolaylaştırmak gerektiğini düşünüyorum. Ama Neden sorgu parametresine koyup da POST ile giden veriye koymadığımı da anlamadım, Query String'in içine koymak da hakkaten apayrı olmamış, düzelteyim onu hakkaten de.

    Ha ama gene de sorarsan ki yaptın mı sen bunu diye? Header kullandım ben :)

    ReplyDelete
  3. Güzel bir yazı olmuş çok teşekkürler.

    ReplyDelete