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.1Query String:
GET /users/bekir?version=1 HTTP/1.1Version Header:
GET /user/123 HTTP/1.1Accept Header:
Host: api.example.com
X-API-Version: 1
Accept: application/json
GET /user/123 HTTP/1.1İş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.
Host: api.example.com
Accept: application/json-v1
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 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/authGeriye 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.
Host: api.example.com
...
username=[apiuser]&pass=[apipass]
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.)
- 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=bekirQuery String Kullanımı:format gibi bir anahtar kelime ile:
GET /v1/users/bekir.json
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=bekirHer üç 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.
Host: api.example.com
Accept: application/json
...
HTTP/1.1 200 OK
Content-Type: application/json
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'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
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=putYa da benzer şekilde HTTP header'larından faydalanabilirsiniz:
POST /users/bekir?method=delete
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/createupdate:
POST /users/bekir/updatedelete:
POST /users/bekir/deleteBu 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/bekirYeni kolksiyon oluşturmak genellikle sık karşılaşılmayan bir durum ancak gene de gerekli ise öğe yaratıeken kullandiginiz yöntemleri izleyebilirsiniz.
HTTP/1.1 201 Created
read
Bir koleksiyon içindeki ögeleri sorgulamak için:GET /v1/usersYa da bir öğeyi edinmek için:
GET /v1/users/bekirkoleksiyon hakkında
Eger kullandığınız koleksiyona dair bilgileralmak isterseniz /info gibi bir path eklentisi kullanabilirsiniz:
GET /v1/users/infokoleksiyonlarda 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=100koleksiyon içerisinde filtreleme/arama
GET /v1/users?page=2&count=100
koleksiyonlarda filtreleme/sorgulama yapmak isterseniz "Query String"lerden faydalanabilirsiniz:
GET /v1/users?uid=1053sonuç içinde istenen alanlar
GET /v1/users?lastname=doğan
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/bekirKı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+sizesilme 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.1Not: 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.
Host: api.exampla.com
...
HTTP/1.1 403 Forbidden
Content-Length: ...
...
{"status": 403, "message": "You are not auhtorized to access this content."}
İ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.
ReplyDeleteAPI 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ı).
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ı.
ReplyDeleteSorgu 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 :)
Güzel bir yazı olmuş çok teşekkürler.
ReplyDelete