İçeriğe geç
academia.sh

Ders 09 / 34

URI Tasarımı

Kaynak adreslerinin yol düzeni: yol ile sorgu bölümünün işbölümü, koleksiyon ve tekil kaynak adlandırması, sabit dilimle parametre diliminin çakışması, iç içe yolların üst dilimi ve kanonik adres kararı.

İçindekiler

Bir önceki ders kaynakları belirledi: kitap, üye, ödünç kaydı, şube ve tabloya karşılık gelmeyen üye durumu. O dersin örneklerinde /kitaplar, /oduncler/1 ve /uyeler/U-1001/durum gibi adresler sorgulanmadan kullanıldı. Oysa her biri bir karardı ve her kararın alternatifi vardı.

Bu ders o kararları açar. Adresin hangi parçası kaynağın kimliğini taşır, hangi parçası görünümünü belirler; koleksiyon adı nasıl yazılır; bir ödünç kaydına iki farklı yoldan başvurulabildiğinde hangisi geçerli sayılır; sondaki eğik çizgi neyi değiştirir. Bunların hepsi, bir eşleyici tablosu çalıştırıldığında ölçülebilir sonuçlar üretir.

Yolun ve Sorgunun İşbölümü

İnternet Nasıl Çalışır kursunda adresin bölümleri tanıtılmıştı: şema, konak, yol, sorgu ve parça. API tasarımında bu bölümler arasında keskin bir işbölümü vardır.

Yol kaynağın kimliğini taşır. /kitaplar/978-0131103627 belirli bir kitabı gösterir; o kitap başka bir yolda bulunmaz. Sorgu bölümü kaynağın görünümünü belirler. /oduncler?durum=acik yeni bir kaynak değildir, ödünç koleksiyonunun süzülmüş bir görünümüdür. Aynı ayrım sıralama, sayfalama ve alan seçimi için de geçerlidir; bunların hepsi sonraki derslerin konusudur ve hepsi sorgu bölümünde yaşar.

Bu işbölümünün pratik sonucu şudur: yolda fiil bulunmaz. /oduncIade, /kitapAra, /uyeSil gibi adresler kaynağı değil işlemi adlandırır. Ne yapılacağını söyleyen şey yöntemdir; yol yalnızca neye yapılacağını söyler. Bir sonraki ders yöntemlerin bu görevi nasıl üstlendiğini ayrıntısıyla ele alır.

Koleksiyon adları çoğul yazılır: /kitaplar, /uyeler, /oduncler. Gerekçe estetik değildir. /kitap/978-0131103627 ile /kitaplar bir arada kullanıldığında istemci tarafındaki her adres kurma noktası, tekil mi çoğul mu olduğunu ayrıca hatırlamak zorunda kalır. Tek bir kural — koleksiyon çoğul, üyesi onun altında — bu hatırlama yükünü sıfırlar. Hangi kuralın seçildiği ikincildir; kurs boyunca değişmemesi birincildir.

Sabit Dilim ile Parametre Diliminin Çakışması

Adres kalıpları bir tablodur ve tablo sırayla taranır. Sıra yanlış olduğunda hata sessizdir. Aşağıdaki eşleyici, hangi kalıbın eşleştiğini yanıtın içinde bildirir; böylece karar görünür olur.

// yol-esleyici.mjs — adres kaliplarini sirayla deneyen esleyici; eslesen kalibi bildirir
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Sira onemlidir: liste yukaridan asagi taranir, ilk eslesen kazanir.
const KALIPLAR = [
  ["/kitaplar/{isbn}", /^\/kitaplar\/([^/]+)$/, (e) =>
    db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(e[1]) ?? null],
  ["/kitaplar/ara",    /^\/kitaplar\/ara$/, () =>
    db.prepare("SELECT isbn, ad FROM kitap WHERE yazar LIKE 'K%'").all()],
  ["/uyeler/{kod}/oduncler/{id}", /^\/uyeler\/([^/]+)\/oduncler\/(\d+)$/, (e) =>
    db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(e[2])) ?? null],
];

const sunucu = createServer((istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  for (const [ad, kalip, isleyici] of KALIPLAR) {
    const eslesme = kalip.exec(yol);
    if (eslesme) return yanitla(yanit, 200, { eslesen: ad, sonuc: isleyici(eslesme) });
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8474, "127.0.0.1", () => console.log("yol esleyici 127.0.0.1:8474"));

Şema, bir önceki derste kurulan sema.sql dosyasıdır; betik veritabanını her çalıştırmada yeniden üretir.

rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node yol-esleyici.mjs & sunucu=$!
sleep 0.4

iste() { printf '%-34s %s\n' "$1" "$(curl -sS "http://127.0.0.1:8474$1")"; }
iste "/kitaplar/978-0131103627"
iste "/kitaplar/ara"
iste "/uyeler/U-1002/oduncler/1"
iste "/kitaplar/978-0131103627/"
iste "/Kitaplar/978-0131103627"

kill $sunucu
yol esleyici 127.0.0.1:8474
/kitaplar/978-0131103627           {"eslesen":"/kitaplar/{isbn}","sonuc":{"isbn":"978-0131103627","ad":"C Programlama Dili","yazar":"Ritchie","yil":1978,"sube":"S-01"}}
/kitaplar/ara                      {"eslesen":"/kitaplar/{isbn}","sonuc":null}
/uyeler/U-1002/oduncler/1          {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}}
/kitaplar/978-0131103627/          {"hata":"yol_yok"}
/Kitaplar/978-0131103627           {"hata":"yol_yok"}

Beş satırda dört ayrı sorun görünür.

İkinci satır çakışmadır. /kitaplar/ara adresi arama kalıbına hiç ulaşmadı; parametre dilimi olan onu önce yakaladı ve ara dizgisini ISBN sanıp veritabanında aradı. Sonuç null döndü — yani istemci, arama uç noktasının bozuk olduğunu değil, aradığı kitabın bulunmadığını düşünür. Uygulama Mimarisi kursunda tanımlanan özgüllük ölçütü burada da geçerlidir: sabit dilimli kalıplar parametre dilimli kalıplardan önce denenmelidir. Sıraya güvenmek istemiyorsanız ikinci yol vardır — arama ölçütünü koleksiyon adresinin sorgu bölümüne taşımak: /kitaplar?yazar=Knuth. Bu yazımda çakışacak bir dilim kalmaz.

Üçüncü satır daha ağırdır. /uyeler/U-1002/oduncler/1 adresine U-1001 üyesinin ödünç kaydı döndü. Kalıp üst dilimi yakaladı ama işleyici onu hiç kullanmadı; yalnızca ile sorgu yaptı. İç içe yol burada süsleme durumuna düşmüştür: adres bir sahiplik iddiası taşır, sunucu o iddiayı denetlemez.

Dördüncü ve beşinci satırlar kanonik biçim sorunudur. Sondaki eğik çizgi ve büyük harf, yolun eşleşmemesine yol açtı. İkisi de “bulunamadı” olarak döndü, oysa kaynak duruyor.

Üst Dilimin Doğrulanması ve Kanonik Biçim

Aşağıdaki eşleyici üç düzeltmeyi birlikte uygular: sabit dilim öne alınır, üst dilim sorguya katılır, sondaki eğik çizgi kanonik adrese yönlendirilir.

// yol-esleyici-2.mjs — ozgullugu once sinayan, ust dilimi dogrulayan esleyici
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Sabit dilimli kaliplar once; parametre dilimi en sonda kalir.
const KALIPLAR = [
  ["/kitaplar/ara",    /^\/kitaplar\/ara$/, () =>
    [200, db.prepare("SELECT isbn, ad FROM kitap WHERE yazar LIKE 'K%'").all()]],
  ["/kitaplar/{isbn}", /^\/kitaplar\/([^/]+)$/, (e) => {
    const k = db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(e[1]);
    return k ? [200, k] : [404, { hata: "kitap_yok" }];
  }],
  ["/uyeler/{kod}/oduncler/{id}", /^\/uyeler\/([^/]+)\/oduncler\/(\d+)$/, (e) => {
    // Ust dilim suslemedir demek yanlistir: kayit o uyeye ait degilse yol yoktur.
    const o = db.prepare("SELECT * FROM odunc WHERE id = ? AND uye = ?").get(Number(e[2]), e[1]);
    return o ? [200, o] : [404, { hata: "odunc_yok" }];
  }],
];

const sunucu = createServer((istek, yanit) => {
  let yol = new URL(istek.url, "http://127.0.0.1").pathname;
  // Kanonik bicim: sondaki egik cizgi tek adrese indirgenir.
  if (yol.length > 1 && yol.endsWith("/")) {
    yanit.writeHead(308, { location: yol.slice(0, -1) });
    return yanit.end();
  }
  for (const [ad, kalip, isleyici] of KALIPLAR) {
    const eslesme = kalip.exec(yol);
    if (eslesme) {
      const [kod, sonuc] = isleyici(eslesme);
      return yanitla(yanit, kod, { eslesen: ad, sonuc });
    }
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8475, "127.0.0.1", () => console.log("duzeltilmis esleyici 127.0.0.1:8475"));
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node yol-esleyici-2.mjs & sunucu=$!
sleep 0.4

iste() { printf '%-30s %3s  %s\n' "$1" \
  "$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8475$1")" \
  "$(curl -sS "http://127.0.0.1:8475$1")"; }
iste "/kitaplar/ara"
iste "/uyeler/U-1002/oduncler/1"
iste "/uyeler/U-1001/oduncler/1"
iste "/kitaplar/978-0131103627/"

# Ayni adres, yonlendirme izlenerek:
printf '%-30s %3s  %s\n' "/kitaplar/978-0131103627/ (-L)" \
  "$(curl -sSL -o /dev/null -w '%{http_code}' http://127.0.0.1:8475/kitaplar/978-0131103627/)" \
  "$(curl -sSL http://127.0.0.1:8475/kitaplar/978-0131103627/)"

kill $sunucu
duzeltilmis esleyici 127.0.0.1:8475
/kitaplar/ara                  200  {"eslesen":"/kitaplar/ara","sonuc":[{"isbn":"978-0201896831","ad":"Programlama Sanati"}]}
/uyeler/U-1002/oduncler/1      404  {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"hata":"odunc_yok"}}
/uyeler/U-1001/oduncler/1      200  {"eslesen":"/uyeler/{kod}/oduncler/{id}","sonuc":{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}}
/kitaplar/978-0131103627/      308
/kitaplar/978-0131103627/ (-L) 200  {"eslesen":"/kitaplar/{isbn}","sonuc":{"isbn":"978-0131103627","ad":"C Programlama Dili","yazar":"Ritchie","yil":1978,"sube":"S-01"}}

İkinci ve üçüncü satır birlikte okunmalıdır: aynı ödünç kaydı, iki farklı üst dilimle iki farklı sonuç verir. Üst dilim artık iddia değil koşuldur. Bu, güvenlik değil doğruluk kararıdır; yetkilendirme ayrı bir konudur ve Kimlik Doğrulama ve Yetkilendirme kursunda ele alınır. Buradaki kazanç şudur: adres neyi söylüyorsa sunucu onu doğrular.

Dördüncü satır yönlendirmeyi gösteriyor: sondaki eğik çizgili adres 308 ile kanonik adrese çevrilir, gövde boştur. Beşinci satırda aynı istek yönlendirme izlenerek yapılır ve 200 ile kaynağa ulaşır. Yönlendirmenin bu türü yöntemi ve gövdeyi korur; bu ayrım, yazma isteklerinin yönlendirilmesi gerektiğinde belirleyici olur.

Büyük harf sorunu bilinçli olarak düzeltilmedi. Yol büyük–küçük harfe duyarlıdır ve duyarlı kalmalıdır; /Kitaplar yolunu da kabul etmek, aynı kaynak için sonsuz sayıda adres üretmek demektir. Doğru karar tek yazımı belgelemek ve diğerlerini reddetmektir.

Kimliğin İçindeki Ayraçlar

Yol dilimlerini ayıran karakter eğik çizgidir; kimlik değeri o karakteri içeriyorsa adres bozulur.

# Kimlikte yol ayraci gecerse ne olur?
node -e '
const kod = "S-01/Merkez";
console.log("ham        :", "/subeler/" + kod);
console.log("kodlanmis  :", "/subeler/" + encodeURIComponent(kod));
const adres = new URL("http://127.0.0.1/subeler/" + encodeURIComponent(kod));
console.log("cozulen    :", decodeURIComponent(adres.pathname.split("/")[2]));
'
ham        : /subeler/S-01/Merkez
kodlanmis  : /subeler/S-01%2FMerkez
cozulen    : S-01/Merkez

Ham yazımda tek bir kimlik iki dilime bölünür ve eşleyici onu alt kaynak sanır. Kodlanmış yazımda dilim bütünlüğünü korur. Bu, istemcinin sorumluluğudur ve sözleşmede yazılı olmalıdır. Daha sağlam olan yol ise kimliğin biçimini baştan kısıtlamaktır: ayraç, boşluk ve büyük harf içermeyen kimlikler bu sorunu hiç doğurmaz.

Kanonik Adres Kararı

Bir kaynağa birden çok yoldan ulaşılabilir: /oduncler/1 ve /uyeler/U-1001/oduncler/1 aynı kaydı verir. İkisi de geçerli olabilir, ama biri kanonik adres olarak seçilmelidir. Seçim ölçütü şudur: kaynağın bağımsız kimliği varsa kanonik adres kısa olandır. Ödünç kaydının kendi kimliği vardır, dolayısıyla /oduncler/1 kanoniktir; iç içe yazım, üyenin kayıtlarını gezerken kullanılan bir kısayoldur.

Kanonik adresin belirlenmesi üç yerde iş görür. Yanıt içinde bir kaynağa bağlantı verilirken hangi biçimin yazılacağı bellidir. Yeni bir kayıt oluşturulduğunda Location başlığına konacak adres bellidir. Önbellek anahtarı olarak adres kullanıldığında aynı kaynak iki farklı anahtarla saklanmaz.

Derinlik de bu ölçütle sınırlanır. /subeler/S-01/uyeler/U-1001/oduncler/1 gibi dört düzeyli bir yol, dört ayrı doğrulama gerektirir ve dördü de her istekte yapılmalıdır. Kural olarak bir düzey yeterlidir: koleksiyonun sahibi olan kaynak, sonra koleksiyon, sonra üyesi. Daha derini gerekiyorsa bu, ilişkinin sorgu ölçütü olarak yazılmasının daha uygun olduğunun işaretidir.

Özet

  • Yol kaynağın kimliğini taşır, sorgu bölümü görünümünü belirler; yolda fiil bulunmaz, ne yapılacağını yöntem söyler.
  • Koleksiyonlar çoğul adlandırılır ve bu karar kurs boyunca değişmez; tutarlılık, hangi kuralın seçildiğinden daha önemlidir.
  • Sabit dilimli kalıplar parametre dilimlilerden önce denenmelidir; aksi hâlde /kitaplar/ara adresi ISBN sanılır ve hata “bulunamadı” gibi görünür.
  • İç içe yolun üst dilimi süsleme değil koşuldur: kayıt o üyeye ait değilse yanıt 404 olmalıdır.
  • Her kaynağın tek bir kanonik adresi belirlenir; sondaki eğik çizgi gibi türevler 308 ile ona yönlendirilir, büyük harf türevleri hiç kabul edilmez.
  • Kimlik değerleri yol ayracı içeriyorsa adres kodlanmalıdır; daha sağlam çözüm kimlik biçimini baştan kısıtlamaktır.

Sonraki Adım

Adresler yerleştiğine göre geriye eylemin nereden geldiği sorusu kalır. Bu derste yol düzeninin fiil taşımadığı söylendi ve “ne yapılacağını yöntem söyler” denip geçildi. Ancak yöntem seçmek bir ad seçmek değildir: her yöntemin, istemcinin ve aradaki bileşenlerin güvendiği özellikleri vardır. Bir okumanın veriyi değiştirmediği, aynı isteğin iki kez gönderilmesinin tek etki ürettiği varsayımları buradan gelir. Sonraki ders bu özellikleri tanımlar ve her birini veritabanındaki satır sayısını sayarak sınar.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat