Ders 17 / 34
Bağlantı Odaklı Yanıtlar
Yanıtın kendi içinde gezinme ve işlem bağlantıları taşıması: sayfaların yalnızca sonraki bağlantısı izlenerek dolaşılması, kaynağın durumuna göre açılıp kapanan işlem bağlantıları ve istemcinin adres kurma yükünün ölçülmesi.
İçindekiler
Dokuz ders boyunca istemcinin adresleri kendisi kurduğu varsayıldı. Koleksiyon adresini, sayfa imlecini, alan listesini, hatta yeni oluşturulan kaydın adresini istemci biliyordu. Bunun bir bedeli var: sözleşmenin biçimi istemcinin içine gömülür. İmleç parametresinin adı değişirse, iade işleminin hangi yöntemle yapıldığı değişirse ya da bir işlem koşula bağlanırsa, her istemci ayrı ayrı güncellenmek zorundadır.
Bu ders bağımlılığı gevşetir. Yanıt, istemcinin bir sonraki isteğinde kullanacağı adresleri kendi içinde taşır; istemci yalnızca giriş adresini bilir, gerisini okur. İkinci ve daha önemli sonuç şudur: kaynağın hangi işlemlere açık olduğu da yanıttan okunur. İade edilmiş bir ödünç kaydı artık iade bağlantısı sunmaz.
Bağlantının Yapısı
Bir bağlantı iki bilgiyi taşımalıdır: hangi adrese ve hangi yöntemle gidileceği. Yalnızca adres taşıyan bir bağlantı, okuma ile yazmayı ayırt edilemez kılar. İşlem bağlantıları için üçüncü bir bilgi daha gerekir: gövdede hangi alanların beklendiği.
Yanıtın hangi bağlantıları taşıyacağı kaynağın türüne göre değişir. Koleksiyon yanıtı gezinme bağlantıları taşır: kendisi ve varsa sonraki sayfa. Tekil kaynak yanıtı ise kendi adresini, bağlı kaynakların adreslerini ve o an uygulanabilir işlemleri taşır.
// baglanti-sunucusu.mjs — yanitlarin icinde gezinme ve islem baglantilari tasidigi servis import { createServer } from "node:http"; import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync("kutuphane.db"); const govdeOku = (istek) => new Promise((coz) => { let veri = ""; istek.on("data", (p) => (veri += p)); istek.on("end", () => coz(veri ? JSON.parse(veri) : {})); }); const yanitla = (yanit, kod, nesne) => { yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" }); yanit.end(JSON.stringify(nesne)); }; // Kaydin o anki durumu, hangi islemlerin acik oldugunu belirler. const kayitGosterimi = (o) => ({ id: o.id, uye: o.uye, isbn: o.isbn, verilis: o.verilis, iade: o.iade, baglantilar: { kendisi: { yol: `/oduncler/${o.id}`, yontem: "GET" }, uye: { yol: `/uyeler/${o.uye}`, yontem: "GET" }, ...(o.iade === null ? { iade: { yol: `/oduncler/${o.id}`, yontem: "PATCH", alanlar: ["iade"] } } : {}), }, }); const sunucu = createServer(async (istek, yanit) => { const adres = new URL(istek.url, "http://127.0.0.1"); const tekil = /^\/oduncler\/(\d+)$/.exec(adres.pathname); const uye = /^\/uyeler\/([\w-]+)$/.exec(adres.pathname); if (istek.method === "GET" && uye) { const u = db.prepare("SELECT * FROM uye WHERE kod = ?").get(uye[1]); return u ? yanitla(yanit, 200, u) : yanitla(yanit, 404, { hata: "uye_yok" }); } if (istek.method === "GET" && adres.pathname === "/oduncler") { const boyut = Math.min(Number(adres.searchParams.get("boyut") ?? 3), 50); const imlec = adres.searchParams.get("imlec"); const satirlar = imlec === null ? db.prepare("SELECT * FROM odunc ORDER BY id LIMIT ?").all(boyut) : db.prepare("SELECT * FROM odunc WHERE id > ? ORDER BY id LIMIT ?").all(Number(imlec), boyut); const son = satirlar.at(-1); // Sonraki sayfa yalnizca gercekten varsa bildirilir. const kalan = son ? db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE id > ?").get(son.id).n : 0; return yanitla(yanit, 200, { veri: satirlar.map(kayitGosterimi), baglantilar: { kendisi: { yol: `/oduncler?boyut=${boyut}`, yontem: "GET" }, ...(kalan > 0 ? { sonraki: { yol: `/oduncler?boyut=${boyut}&imlec=${son.id}`, yontem: "GET" } } : {}), }, }); } if (istek.method === "GET" && tekil) { const o = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(tekil[1])); return o ? yanitla(yanit, 200, kayitGosterimi(o)) : yanitla(yanit, 404, { hata: "odunc_yok" }); } if (istek.method === "PATCH" && tekil) { const g = await govdeOku(istek); const id = Number(tekil[1]); if ("iade" in g) db.prepare("UPDATE odunc SET iade = ? WHERE id = ?").run(g.iade, id); return yanitla(yanit, 200, kayitGosterimi(db.prepare("SELECT * FROM odunc WHERE id = ?").get(id))); } yanitla(yanit, 404, { hata: "yol_yok" }); }); sunucu.listen(8484, "127.0.0.1", () => console.log("baglanti sunucusu 127.0.0.1:8484"));
İki ayrıntı özellikle önemlidir. sonraki bağlantısı yalnızca gerçekten kalan kayıt varsa
üretiliyor; istemcinin “boş sayfa gelirse dur” gibi bir kural yazmasına gerek kalmıyor.
iade bağlantısı ise yalnızca kaydın iade alanı boşken üretiliyor; iş kuralı gösterimin
içine yerleşmiş oluyor.
Yalnızca Giriş Adresini Bilen İstemci
Karşı taraftaki istemci tek bir adres yazıyor ve başka hiçbir adres kurmuyor.
// gezgin.mjs — yalnizca giris adresini bilen, geri kalanini yanittan okuyan istemci const TABAN = "http://127.0.0.1:8484"; const GIRIS = "/oduncler?boyut=3"; const kurulanAdres = 1; // yalnizca giris adresi istemcide yaziliyor const cagir = async (baglanti, govde) => { const secenek = { method: baglanti.yontem }; if (govde) { secenek.headers = { "content-type": "application/json" }; secenek.body = JSON.stringify(govde); } return (await fetch(TABAN + baglanti.yol, secenek)).json(); }; // 1) Sayfalar yalnizca "sonraki" baglantisi izlenerek dolasilir. let sayfa = await (await fetch(TABAN + GIRIS)).json(); let sayfaSayisi = 1; const kayitlar = []; while (true) { kayitlar.push(...sayfa.veri); console.log(`sayfa ${sayfaSayisi}: ${sayfa.veri.map((k) => k.id).join(", ")}` + ` sonraki: ${sayfa.baglantilar.sonraki?.yol ?? "yok"}`); if (!sayfa.baglantilar.sonraki) break; sayfa = await cagir(sayfa.baglantilar.sonraki); sayfaSayisi++; } console.log(`toplam ${kayitlar.length} kayit, ${sayfaSayisi} sayfa`); // 2) Islem baglantisi acik olan ilk kayit bulunur ve o baglanti izlenir. const acik = kayitlar.find((k) => k.baglantilar.iade); console.log(`\nislem oncesi id=${acik.id} baglantilar: ${Object.keys(acik.baglantilar).join(", ")}`); const guncel = await cagir(acik.baglantilar.iade, { iade: "2026-03-15" }); console.log(`islem sonrasi id=${guncel.id} baglantilar: ${Object.keys(guncel.baglantilar).join(", ")}`); // 3) Kaydin kendi adresi de yanittan okunur. const yeniden = await cagir(guncel.baglantilar.kendisi); console.log(`yeniden okuma id=${yeniden.id} iade=${yeniden.iade} ` + `baglantilar: ${Object.keys(yeniden.baglantilar).join(", ")}`); console.log(`\nistemcide kurulan adres sayisi: ${kurulanAdres}`);
# Yedi odunc kaydi; istemci yalnizca giris adresini bilerek butun akisi yurutur. rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql sqlite3 kutuphane.db <<'SQL' DELETE FROM odunc; INSERT INTO odunc (id, uye, isbn, verilis, iade) VALUES (1,'U-1001','K-01','2026-02-01','2026-02-14'), (2,'U-1002','K-02','2026-02-05','2026-02-19'), (3,'U-1001','K-03','2026-03-01',NULL), (4,'U-1002','K-04','2026-03-02',NULL), (5,'U-1001','K-05','2026-03-03',NULL), (6,'U-1002','K-06','2026-03-04',NULL), (7,'U-1001','K-07','2026-03-05',NULL); SQL node baglanti-sunucusu.mjs & sunucu=$! sleep 0.4 node gezgin.mjs echo "--- tek kaydin ham gosterimi ---" curl -sS http://127.0.0.1:8484/oduncler/4; echo kill $sunucu
baglanti sunucusu 127.0.0.1:8484
sayfa 1: 1, 2, 3 sonraki: /oduncler?boyut=3&imlec=3
sayfa 2: 4, 5, 6 sonraki: /oduncler?boyut=3&imlec=6
sayfa 3: 7 sonraki: yok
toplam 7 kayit, 3 sayfa
islem oncesi id=3 baglantilar: kendisi, uye, iade
islem sonrasi id=3 baglantilar: kendisi, uye
yeniden okuma id=3 iade=2026-03-15 baglantilar: kendisi, uye
istemcide kurulan adres sayisi: 1
--- tek kaydin ham gosterimi ---
{"id":4,"uye":"U-1002","isbn":"K-04","verilis":"2026-03-02","iade":null,"baglantilar":{"kendisi":{"yol":"/oduncler/4","yontem":"GET"},"uye":{"yol":"/uyeler/U-1002","yontem":"GET"},"iade":{"yol":"/oduncler/4","yontem":"PATCH","alanlar":["iade"]}}}
Üç sayfa, yedi kayıt, tek bir yazılı adres. İstemcinin kodunda imlec sözcüğü hiç geçmiyor;
imlecin nasıl kodlandığı, hangi parametre adıyla taşındığı, hatta sayfalamanın imleç tabanlı
mı atlama tabanlı mı olduğu istemciyi ilgilendirmiyor. Sunucu bu kararı değiştirdiğinde
istemci değişmez.
İkinci bölüm daha ilginçtir. İade işleminden önce kaydın bağlantıları arasında iade
vardı; işlemden sonra kayboldu. İstemci “bu kayıt iade edilebilir mi” sorusunu bir alanın
değerine bakıp kendi kuralıyla yanıtlamıyor; bağlantının varlığına bakıyor. Kural sunucuda
duruyor ve tek yerde duruyor.
Bunun arayüz tarafındaki karşılığı doğrudandır: iade düğmesinin görünürlüğü, iade
bağlantısının varlığına bağlanır. İş kuralı değiştiğinde — örneğin cezalı üyelerin iade
işlemini şubede yapması gerektiğinde — sunucu bağlantıyı üretmez ve düğme kendiliğinden
kaybolur.
Ham Gösterim ve Sözleşmenin Sınırı
Çıktının son satırı tek bir kaydın ham gösterimini veriyor. Bağlantılar gövdenin ayrı bir
alanında toplanmış; kaydın kendi alanlarıyla karışmıyor. Bu ayrım korunmalıdır, yoksa
iade adı hem bir tarih alanı hem de bir bağlantı adı olur.
Bağlantı odaklı tasarımın da bir maliyeti vardır ve abartılmamalıdır.
Gövde büyür. Yedi kayıtlık bir listede her kaydın üç bağlantı taşıması, gövdenin önemli bir bölümünü bağlantılara ayırır. Kısmi yanıt dersindeki ölçüt burada da geçerlidir: bağlantı bloğu alan seçimiyle kapatılabilmelidir.
İstemci bağımlılığı yok olmaz, yer değiştirir. İstemci artık adres biçimine değil,
bağlantı adlarına bağımlıdır. sonraki adının değişmesi, imleç parametresinin adının
değişmesi kadar kırıcıdır. Kazanç, bağlantı adlarının adres biçimlerinden daha az
değişmesindedir.
Bağlantı üretmek sunucuya iş çıkarır. Yukarıdaki koleksiyon uç noktası, sonraki sayfanın var olup olmadığını anlamak için fazladan bir sayım sorgusu çalıştırıyor. Büyük koleksiyonlarda bu sayım pahalıdır; yaygın çözüm, sayfa boyutundan bir fazla kayıt çekip fazlalık gelirse sonraki sayfanın var olduğuna karar vermektir.
Son olarak, bağlantıların hepsi aynı düzeyde zorunlu değildir. Gezinme bağlantıları olmadan istemci sayfalama biçimini bilmek zorunda kalır; işlem bağlantıları olmadan iş kurallarını yeniden yazmak zorunda kalır. İlki neredeyse her serviste karşılığını verir; ikincisi, iş kurallarının karmaşık ve değişken olduğu servislerde değerlidir.
Özet
- Bağlantı en az iki bilgi taşır: adres ve yöntem; işlem bağlantıları ayrıca gövdede beklenen alanları bildirir.
- Koleksiyon yanıtı gezinme bağlantıları, tekil kaynak yanıtı bağlı kaynak ve işlem bağlantıları taşır; bağlantılar gövdenin ayrı bir alanında toplanır.
- Ölçümde istemci tek bir adres yazarak üç sayfayı dolaştı ve yedi kaydı topladı; imleç parametresinin adı istemcinin kodunda hiç geçmedi.
- İşlem bağlantıları kaynağın o anki durumundan üretilir: iade edilmiş kayıtta
iadebağlantısı yoktur, dolayısıyla iş kuralı tek yerde durur. - Sonraki sayfa bağlantısı yalnızca kalan kayıt varken üretilir; istemcinin boş sayfayla durma kuralı yazmasına gerek kalmaz.
- Maliyeti gövdenin büyümesi, sunucunun bağlantı üretme yükü ve bağımlılığın adres biçiminden bağlantı adlarına kaymasıdır.
Sonraki Adım
Kaynak ve sözleşme tasarımı tamamlandı: kaynaklar belirlendi, adresleri yerleşti,
yöntemler ve durum kodları eşlendi, gövde biçimi kararlaştırıldı, koleksiyonlar sayfalandı
ve süzüldü, yazma istekleri yinelenen etkiye karşı korundu, yanıtlar kendi gezinme
bilgisini taşır oldu. Bütün bunlar işler yolunda gittiğinde geçerlidir. Hata durumunda ise
her uç nokta kendi gövdesini uyduruyor: bir yerde hata alanı, başka yerde alan ve
neden, bir başka yerde yalnız bir kod. Sonraki konu buradan başlar ve ilk dersi hata
yanıtlarının tek bir yapıya oturtulmasıdır: standart bir problem ayrıntısı biçimi, hangi
alanları taşır ve istemci onu nasıl kullanır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.