REST API, bir sunucudaki verilerin kaynaklar (resources) olarak düşünüldüğü, her kaynağın bir URL ile tanımlandığı ve kaynak üzerinde yapılacak işlemin URL'de değil HTTP metodunda söylendiği bir arayüzdür. GET /urunler/7 ürünü okur, DELETE /urunler/7 aynı ürünü siler: adres değişmez, değişen yalnızca metottur. Sunucu da kaynağın kendisini değil, o anki durumunu anlatan bir temsili (representation), genellikle bir JSON belgesi gönderir.

Bu yazıda kaynak, temsil ve tekdüze arayüz (uniform interface) kavramlarını HTTP'nin anlamını tanımlayan RFC 9110: HTTP Semantics üzerinden açıklıyor, eylem tabanlı URL'lerle kaynak tabanlı URL'lerin farkını örnek isteklerle gösteriyoruz. Metotların tek tek tanımı ve hangi metodun güvenli ya da idempotent olduğu HTTP istek ve yanıt yapısı yazısında anlatıldığı için burada yalnızca tasarıma etkisini ele alıyoruz.

REST kelimesi nereden geliyor?

REST, Representational State Transfer, yani temsil yoluyla durum aktarımı ifadesinin kısaltmasıdır. Terim, Roy Fielding'in 2000 yılında yayımlanan ağ tabanlı yazılım mimarileri üzerine doktora tezinden gelir. HTTP'nin güncel anlam belgesi RFC 9110, bu teze doğrudan kaynak olarak atıf yapar: standart HTTP metotlarının belirli bir kaynağa özgü olmadığını, aynı metodun her kaynakta aynı anlamı taşıdığını ve bu tekdüze arayüzün ağ tabanlı sistemlerde görünürlüğü ve yeniden kullanımı artırdığını söylerken tezi gösterir.

Bu ayrıntı, REST'i anlamanın anahtarıdır. Bir nesneye uzaktan metot çağırdığınız sistemlerde her nesnenin kendine özgü metotları olur: siparisOlustur, kullaniciyiSil, urunFiyatiniGuncelle. HTTP'de ise metot listesi sabittir ve herkes için aynıdır. Çeşitlilik metotlarda değil, kaynaklardadır. Bir REST API tasarlarken yaptığınız iş, iş alanınızdaki şeyleri kaynaklara dönüştürmek ve bu sabit metot kümesini onlara uygulamaktır.

Kaynak nedir?

RFC 9110'a göre bir HTTP isteğinin hedefine kaynak denir ve HTTP kaynağın doğasına hiçbir sınır koymaz; yalnızca kaynakla etkileşim için bir arayüz tanımlar. Kaynakların çoğu bir URI ile tanımlanır. Pratikte kaynak, adlandırabildiğiniz ve üzerinde konuşabildiğiniz her şeydir: bir kullanıcı, bir sipariş, bir ürün listesi, hatta belgede örnek verilen "şu anki hava durumu" gibi zamanla değişen bir kavram.

Mağaza örneğimizde kaynaklar iki türlüdür. Koleksiyon kaynakları birden çok öğeyi temsil eder: /urunler bütün ürünler, /siparisler bütün siparişlerdir. Tekil kaynaklar koleksiyonun bir üyesidir: /urunler/7 kimliği 7 olan üründür. Bu ikili yapı, REST API'lerin büyük bölümünün iskeletini oluşturur ve CRUD endpoint tasarımı yazısında ayrıntılı olarak kurulur.

Kaynak, veritabanı tablosuyla aynı şey olmak zorunda değildir. RFC 9110, sunucunun kaynak durumunu nasıl sakladığını ve temsillere nasıl dönüştürdüğünü bilerek gizlediğini vurgular. Bir sipariş kaynağı arka planda üç tablonun birleşiminden üretilebilir; istemci bunu bilmez, bilmesine de gerek yoktur. Bu gizleme, sunucu tarafında veri modelini değiştirdiğinizde istemcileri bozmadan ilerleyebilmenizi sağlar.

Temsil nedir ve neden önemlidir?

Temsil, bir kaynağın geçmişteki, şu anki ya da istenen durumunu protokol üzerinden kolayca taşınabilecek bir biçimde yansıtan bilgidir. RFC 9110 bunu temsil üst verisi (örneğin Content-Type) ile temsil verisinin (gövdenin) birleşimi olarak tanımlar. İstemci ürünün kendisini almaz; ürünün o anki durumunu anlatan bir JSON belgesi alır:

json
{"id": 7, "ad": "Klavye", "fiyat": 450, "stokta": true}

Aynı kaynağın birden çok temsili olabilir: aynı ürün JSON olarak da, XML olarak da sunulabilir. Hangisinin seçileceğine çoğunlukla içerik anlaşması (content negotiation) karar verir; istemci Accept başlığıyla tercihini bildirir. Bu konu content negotiation yazısının kapsamındadır. Tasarım açısından çıkarılacak ders şudur: URL kaynağı tanımlar, biçimi değil. /urunler/7.json ve /urunler/7.xml gibi biçime göre ayrılmış adresler yerine tek adres ve başlıkla seçilen temsil, REST'in temsil kavramıyla daha uyumludur.

İstemci de sunucuya temsil gönderir. Bir ürünü güncellerken gövdeye koyduğunuz JSON, ürünün istenen durumunun temsilidir. PUT isteğinde bu temsil kaynağın yeni durumu olarak kabul edilir; POST isteğinde ise kaynak gelen temsili kendi kurallarına göre işler. Bu fark PUT ve PATCH yazısında ve CRUD yazısında daha yakından incelenir.

Kaynağı tanımlamak ile işlemi söylemek ayrı işlerdir

Kaynağı tanımlamak ile işlemi söylemek ayrı işlerdir: Ara katmanlar metodu anlar, URL'deki fiili anlamaz, Uç nokta sayısı ve adlandırma tutarsızlığı, Tekrar deneme ve idempotentlik

RFC 9110, HTTP'nin tasarım hedeflerinden birinin kaynak tanımlamayı istek anlamından ayırmak olduğunu açıkça yazar. İsteğin anlamı metotta ve birkaç başlıkta taşınır; URL yalnızca hangi kaynağın hedef alındığını söyler. Belge bir adım daha ileri gider: bir kaynak, isteği metodun anlamıyla çelişecek biçimde işleyemez. URL güvensiz bir işlem çağrıştırsa bile, güvenli bir metotla (örneğin GET) gelen istekte istemci sunucunun durum değiştirmemesini bekleyebilir.

Bu kuralın tasarımdaki karşılığı nettir. Aşağıdaki tablo aynı işlemleri iki farklı yaklaşımla yazar:

İşlemEylem tabanlı URLKaynak tabanlı URL
Ürünleri listeleGET /getProductsGET /urunler
Bir ürünü getirGET /getProduct?id=7GET /urunler/7
Ürün oluşturPOST /createProductPOST /urunler
Ürünü değiştirPOST /updateProductPUT /urunler/7
Ürünü silPOST /deleteProductDELETE /urunler/7

Eylem tabanlı sütunda her işlem için yeni bir adres uydurulmuş, fiil adresin içine taşınmıştır. Kaynak tabanlı sütunda ise yalnızca iki adres vardır: koleksiyon ve tekil ürün. Fiil görevini metot üstlenir. Bu yaklaşımın REST API'lerde yaygın bir tasarım tercihi olmasının birkaç somut gerekçesi var.

Ara katmanlar metodu anlar, URL'deki fiili anlamaz

Önbellekler, vekil sunucular (proxy) ve istemci kütüphaneleri isteğin ne yaptığını metottan çıkarır. RFC 9110, güvenli ve güvensiz metot ayrımının amacını, otomatik tarayıcıların (spider) ve ön yükleme yapan önbelleklerin zarar verme korkusu olmadan çalışabilmesi olarak açıklar. Bir sistem GET isteklerini güvenle tekrarlar ya da önceden getirir. Silme işlemini GET /deleteProduct?id=7 gibi bir adrese koyarsanız, bağlantıyı önceden yüklemeye çalışan bir araç ürününüzü silebilir. Bütün işlemleri POST ile yaparsanız da tersi olur: zararsız okuma istekleri bile güvensiz görünür ve tekrar denenmez.

Uç nokta sayısı ve adlandırma tutarsızlığı

Eylem tabanlı tasarımda her yeni işlem yeni bir ad gerektirir ve adlar zamanla dağılır: bir ekip getProduct, başka biri fetchOrder, bir diğeri listUsersAll yazar. Kaynak tabanlı tasarımda yeni bir kaynak eklediğinizde istemcinin onu nasıl kullanacağı zaten bellidir: koleksiyona GET listeler, POST oluşturur; tekil adrese GET, PUT, PATCH, DELETE uygulanır. Ad kuralları endpoint isimlendirme yazısında ayrıntılı olarak anlatılır.

Tekrar deneme ve idempotentlik

RFC 9110, idempotent metotlarla yapılan isteklerin, istemci yanıtı okuyamadan bağlantı koptuğunda otomatik olarak tekrarlanabileceğini söyler; idempotent olmayan istekler ise istemci isteğin gerçekten idempotent olduğunu bilmiyorsa otomatik olarak tekrarlanmamalıdır. DELETE /urunler/7 isteğinin iki kez gitmesi aynı sonucu doğurur, bu yüzden istemci gönül rahatlığıyla tekrar dener. Silme işlemini POST /deleteProduct ile yaparsanız istemci bunu bilemez. Tekrarlanması riskli POST istekleri için ayrı bir çözüm vardır: Idempotency-Key başlığı.

Bir isteğin yolculuğu: küçük bir örnek

Kavramları bir araya getirelim. İstemci 7 numaralı ürünü okur, siler ve tekrar okumaya çalışır. İlk istek ve yanıt şöyledir:

http
GET /urunler/7 HTTP/1.1
Host: api.example.com
Accept: application/json
http
HTTP/1.1 200 OK
Content-Type: application/json

{"id": 7, "ad": "Klavye", "fiyat": 450}

Silme isteği aynı adrese gider, yalnızca metot değişir. Sunucu işlemi yaptığını ve gönderecek ek içerik olmadığını 204 ile bildirir:

http
DELETE /urunler/7 HTTP/1.1
Host: api.example.com
http
HTTP/1.1 204 No Content

Aynı adrese yapılan yeni bir GET artık 404 Not Found döner, çünkü bu adreste kaynağın güncel bir temsili yoktur. Bu akışı FastAPI ile kurup test istemcisiyle çalıştırdığımızda çıktılar şöyle olur:

python
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

app = FastAPI()
urunler = {7: {"id": 7, "ad": "Klavye", "fiyat": 450}}

@app.get("/urunler/{urun_id}")
def urun_getir(urun_id: int):
    if urun_id not in urunler:
        raise HTTPException(status_code=404, detail="Ürün bulunamadı")
    return urunler[urun_id]

@app.delete("/urunler/{urun_id}", status_code=204)
def urun_sil(urun_id: int):
    urunler.pop(urun_id, None)

c = TestClient(app)
r = c.get("/urunler/7")
print(r.status_code, r.headers["content-type"])  # çıktı: 200 application/json
print(r.json())  # çıktı: {'id': 7, 'ad': 'Klavye', 'fiyat': 450}
print(c.delete("/urunler/7").status_code)  # çıktı: 204
print(c.get("/urunler/7").status_code)  # çıktı: 404
print(c.get("/urunler/abc").status_code)  # çıktı: 422

Son satır ilginç bir ayrıntı gösterir: /urunler/abc adresinde kimlik bir tam sayı olmadığı için FastAPI isteği işlevi çağırmadan 422 ile reddeder. Bu, çatının yol parametresini tipine göre doğruladığını gösterir. Bu tür bir hatada hangi kodun döneceğini kendiniz belirlemek isteyebilirsiniz; 400 ile 422 arasındaki seçim istek doğrulama yazısında anlatılır. Örnekte veriler bir sözlükte tutuluyor; gerçek bir API'de bu bir veritabanıdır, tasarım kararları ise aynıdır.

Kısa alıştırma: bir özellikten kaynakları çıkarmak

Kaynak odaklı düşünmeyi alıştırmanın iyi bir yolu, bir özellik tanımındaki isimlerin altını çizmektir. "Kullanıcı sepetine ürün ekler, sepeti siparişe dönüştürür, siparişin kargo durumunu takip eder" cümlesinde isimler kullanıcı, sepet, ürün, sipariş ve kargo durumudur. Fiiller (ekler, dönüştürür, takip eder) ise metotlara ve kaynak durumlarındaki değişikliklere karşılık gelir:

bash
GET    /urunler                  # ürünleri listele
POST   /sepetler/5/kalemler      # sepete ürün ekle
DELETE /sepetler/5/kalemler/3    # sepetten çıkar
POST   /siparisler               # sepeti siparişe dönüştür (gövdede sepet_id)
GET    /siparisler/1001          # sipariş ve kargo durumunu oku

"Siparişe dönüştürmek" bir fiil olduğu hâlde URL'ye /sepetiSipariseDonustur diye yazılmadı; işlem, sipariş koleksiyonuna yeni bir kayıt eklemek olarak modellendi. Kargo durumu ayrı bir kaynak olabilir ya da siparişin bir alanı olarak okunabilir; ikisi de geçerlidir ve karar, istemcinin bu bilgiye ne sıklıkla ve hangi ayrıntıda ihtiyaç duyduğuna bağlıdır. İç içe yolların ne kadar derin olabileceği iç içe kaynaklar yazısında anlatılır.

Bir REST API'yi oluşturan parçalar

Bir REST API'yi oluşturan parçalar: Kaynak ve URL, Metot, Temsil ve medya tipi, Durum kodu

Örnekteki her mesajda aynı beş parça vardır ve tasarım kararlarınızın hepsi bunlardan birine düşer:

  • Kaynak ve URL: neyin hedef alındığı. Çoğul isimler, kısa hiyerarşi, kimlik yolda.
  • Metot: ne yapılmak istendiği. Sabit bir küme; anlamı her kaynakta aynı.
  • Temsil ve medya tipi: verinin biçimi. Gövde ve Content-Type başlığı.
  • Durum kodu: sonucun ne olduğu. 2xx başarı, 4xx istemci hatası, 5xx sunucu hatası.
  • Başlıklar: isteği ve yanıtı niteleyen ek bilgi. Location, Authorization, Accept gibi.

Durum kodlarının senaryoya göre seçimi durum kodu seçimi yazısında, yanıt gövdesinin yapısı JSON yanıt yapısı yazısında ele alınır.

Sık yapılan yanlış anlamalar

  • "JSON dönen her HTTP API REST'tir." Biçim tek başına yeterli değildir. Bütün işlemleri POST /api adresine gönderip gövdedeki bir alanla hangi işin yapılacağını seçen bir API JSON kullanır ama kaynak odaklı değildir.
  • "Kaynak, veritabanı tablosudur." Kaynak istemcinin gördüğü kavramdır. Bir tabloyu birden çok kaynakla sunabilir ya da birkaç tabloyu tek bir kaynakta birleştirebilirsiniz.
  • "URL'deki fiil daha açıklayıcıdır." Metot zaten fiildir. POST /createOrder aynı bilgiyi iki kez söyler, üstelik ara katmanların anlayamayacağı bir yerde.
  • "GET ile küçük bir değişiklik yapmak sorun olmaz." RFC 9110'a göre istemci güvenli bir metotla durum değişikliği istemez ve beklemez. Durum değiştiren bir işlemi GET'e bağlamak, önbellek ve tarayıcı davranışlarıyla beklenmedik sonuçlar doğurabilir.
  • "REST, GraphQL'in eski hâlidir." İkisi farklı yaklaşımlardır; karşılaştırma için GraphQL ve REST yazısına bakın.

Sık Sorulan Sorular

REST ile RESTful arasında fark var mı?

Günlük kullanımda ikisi aynı anlamda kullanılır: RESTful, REST ilkelerine uygun tasarlanmış demektir. Bu seride ikisini ayırmıyoruz.

REST API'nin JSON kullanması zorunlu mu?

Hayır. HTTP temsilin biçimini sınırlamaz; aynı kaynak JSON, XML ya da başka bir biçimde sunulabilir. JSON, okuması ve ayrıştırması kolay olduğu için yaygın bir varsayılandır.

Her işlem bir kaynağa uyar mı?

Çoğu işlem uyar, bazıları zorlanır. Bir siparişi iptal etmek gibi işlemleri kaynağın durumunu değiştiren bir güncelleme olarak modellemek mümkündür. Bu tür sınır durumlar isimlendirme yazısında tartışılır.

Bir REST API'yi denemek için sunucu çalıştırmam gerekir mi?

Tasarımı sınamak için gerekmez. Bu yazıdaki örnek, FastAPI'nin test istemcisiyle hiçbir port açmadan istek gönderiyor ve durum kodlarını gösteriyor. Yayındaki bir API'yi ise curl ya da benzeri bir araçla, ham HTTP mesajlarını görerek denemek en açıklayıcı yoldur.

Aynı adres zamanla farklı veri döndürebilir mi?

Evet, bu beklenen bir durumdur. RFC 9110, temsilin kaynağın o anki durumunu yansıttığını ve kaynağın zamanla değişen bir kavram olabileceğini söyler. /urunler/7 fiyat değiştiğinde yeni fiyatı döndürür; adres aynı kalır, temsil değişir. Önbelleklerin bu değişikliği nasıl izlediği ETag ve koşullu istekler yazısında anlatılır.

REST API ile web sitesi arasında ne fark var?

İkisi de HTTP kullanır. Web sayfası insanlara HTML temsili sunar; REST API ise programlara, genellikle JSON temsili sunar. Kaynak, metot ve durum kodu kavramları ikisinde de aynıdır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: REST API tasarımı öğrenme rehberi · Endpoint isimlendirme kuralları · CRUD endpoint tasarımı · HTTP istek ve yanıt yapısı · GraphQL ve REST