---
title: 'Kaynak Modelleme'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/kaynak-modelleme'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:45+00:00'
license: 'CC BY-SA 4.0'
---

# Kaynak Modelleme

Alan kavramlarının adreslenebilir kaynağa dönüşmesi: eylem adlarıyla büyüyen uç nokta listesi, işlemin durum değişimi olarak modellenmesi, hiçbir tabloya karşılık gelmeyen hesaplanmış kaynaklar ve kimlik seçiminin bedeli.

API Biçemleri konusu seçim işini bitirdi: kaynak temelli, uzak yordam çağrısı ve sorgu
temelli yaklaşımların hangi kısıt altında birbirine üstün geldiği, eşzamanlı ve eşzamansız
iletişimin nerede ayrıştığı kuruldu. Kütüphane ödünç servisi için karar kaynak temelli
biçemden yana verildiğinde iş bitmez; asıl tasarım o noktada başlar. Biçem, "kaynakları
adresle ve tek biçim arayüzle konuş" der, ama hangi şeyin kaynak olduğunu söylemez.

Bu ders o boşluğu doldurur. Elde bir alan vardır — kitap, üye, ödünç işlemi, şube, gecikme
ücreti, ödünç süresi — ve bu kavramların hepsi aynı türden değildir. Bazıları adreslenir,
bazıları yalnızca bir kaynağın içinde bir alan olarak yaşar, bazıları da hiçbir tabloda
satırı olmadan kaynak olur. Ayrımı yapan ölçüt kimliktir.

## Kaynak Nedir, Ne Değildir

**Kaynak** (resource), istemcinin ayrı ayrı başvurmak isteyeceği ve zaman içinde aynı
kalan bir kimliği olan şeydir. Bu tanımın iki yarısı da işlevseldir. Kimliği olmayan bir
şey adreslenemez; başvurulmayan bir şeyin adreslenmesi de yalnız yüzey büyütür.

Ödünç servisinde ölçütü uygulayalım. **Kitap** kaynaktır: katalogda tek başına aranır,
ayrıntısı gösterilir, güncellenir. **Üye** kaynaktır. **Ödünç işlemi** kaynaktır — bu, ilk
bakışta bir eylem gibi görünse de kendi kimliği, başlangıcı, bitişi ve tarihçesi olan bir
şeydir. **Şube** kaynaktır. Buna karşılık **ödünç süresi** kaynak değildir: on dört günlük
süre bir kuraldır, ödünç kaydının içinde ya da servisin yapılandırmasında yaşar.
**Gecikme ücreti** de tek başına kaynak değildir; bir üyenin borcunun bileşenidir.

Kaynağın kendisi soyuttur. İstemciye giden JSON gövde, kaynağın o andaki **gösterimidir**
(representation). Aynı kaynağın birden çok gösterimi olabilir: özet liste satırı ile
ayrıntı sayfası aynı ödünç kaydını farklı ayrıntı düzeyinde anlatır. Kaynak ile gösterimi
ayırmak, sonraki derslerde alan seçimi ve kısmi yanıt konularını mümkün kılan ayrımdır.

## Eylemleri Adres Adı Yapmanın Bedeli

Kaynak düşüncesi olmadan bir servis yazıldığında adresler eylem adlarına dönüşür:
`/oduncVer`, `/oduncIade`, `/uyeninOduncleri`. Her yeni istemci ihtiyacı yeni bir yol açar.
Aşağıdaki iki sunucu aynı verinin üstünde aynı soruları yanıtlar; farkları adres kümesinin
nasıl büyüdüğüdür.

```js
// eylem-sunucusu.mjs — her istemci ihtiyacina bir yol acan tasarim
import { createServer } from "node:http";

const KITAPLAR = [
  { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" },
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" },
];
const ODUNCLER = [
  { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null },
  { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" },
];

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

// Her satir bir istemci ihtiyaci; ihtiyac artinca satir sayisi artar.
const YOLLAR = {
  "/kitapListesi":      () => KITAPLAR,
  "/kitapDetay":        (s) => KITAPLAR.find((k) => k.isbn === s.get("isbn")) ?? null,
  "/kitapEkle":         () => ({ sonuc: "eklendi" }),
  "/kitapSil":          () => ({ sonuc: "silindi" }),
  "/oduncListesi":      () => ODUNCLER,
  "/oduncDetay":        (s) => ODUNCLER.find((o) => o.id === Number(s.get("id"))) ?? null,
  "/oduncVer":          () => ({ sonuc: "verildi" }),
  "/oduncIade":         () => ({ sonuc: "iade alindi" }),
  "/uyeninOduncleri":   (s) => ODUNCLER.filter((o) => o.uye === s.get("uye")),
  "/subedekiKitaplar":  (s) => KITAPLAR.filter((k) => k.sube === s.get("sube")),
  "/acikOduncler":      () => ODUNCLER.filter((o) => o.iade === null),
};

const sunucu = createServer((istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  if (adres.pathname === "/yollar") return yanitla(yanit, 200, { sayi: Object.keys(YOLLAR).length });
  const isleyici = YOLLAR[adres.pathname];
  if (!isleyici) return yanitla(yanit, 404, { hata: "yol_yok" });
  yanitla(yanit, 200, isleyici(adres.searchParams));
});

sunucu.listen(8471, "127.0.0.1", () => console.log("eylem sunucusu 127.0.0.1:8471"));
```

```js
// kaynak-sunucusu.mjs — ayni ihtiyaclari kaynak adresleriyle karsilayan tasarim
import { createServer } from "node:http";

const KITAPLAR = [
  { isbn: "978-0201896831", ad: "Programlama Sanati", yazar: "Knuth", sube: "S-01" },
  { isbn: "978-0262033848", ad: "Algoritmalara Giris", yazar: "Cormen", sube: "S-02" },
];
const ODUNCLER = [
  { id: 1, uye: "U-1001", isbn: "978-0262033848", iade: null },
  { id: 2, uye: "U-1002", isbn: "978-0201896831", iade: "2026-03-04" },
];

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

// Uc adres kalibi; suzme olcutleri sorgu bolumunde tasinir.
const KALIPLAR = [
  [/^\/kitaplar$/, (e, s) => {
    const sube = s.get("sube");
    return sube ? KITAPLAR.filter((k) => k.sube === sube) : KITAPLAR;
  }],
  [/^\/kitaplar\/([\w-]+)$/, (e) => KITAPLAR.find((k) => k.isbn === e[1]) ?? null],
  [/^\/oduncler$/, (e, s) => {
    let sonuc = ODUNCLER;
    if (s.get("uye")) sonuc = sonuc.filter((o) => o.uye === s.get("uye"));
    if (s.get("durum") === "acik") sonuc = sonuc.filter((o) => o.iade === null);
    return sonuc;
  }],
  [/^\/oduncler\/(\d+)$/, (e) => ODUNCLER.find((o) => o.id === Number(e[1])) ?? null],
];

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

sunucu.listen(8472, "127.0.0.1", () => console.log("kaynak sunucusu 127.0.0.1:8472"));
```

Her iki sunucu da uzun ömürlüdür; aşağıdaki betik ikisini de başlatır, aynı soruları
sorar ve sonunda durdurur.

```bash
# Iki sunucuyu sirayla baslatir, ayni dort soruyu her ikisine sorar, durdurur.
node eylem-sunucusu.mjs & eylem=$!
sleep 0.4
node kaynak-sunucusu.mjs & kaynak=$!
sleep 0.4

sor() { printf '%-22s %s\n' "$1" "$(curl -sS "$2")"; }

echo "--- eylem tabanli ---"
sor "sube S-01 kitaplari" "http://127.0.0.1:8471/subedekiKitaplar?sube=S-01"
sor "U-1001 oduncleri"    "http://127.0.0.1:8471/uyeninOduncleri?uye=U-1001"
sor "acik oduncler"       "http://127.0.0.1:8471/acikOduncler"
sor "yol sayisi"          "http://127.0.0.1:8471/yollar"

echo "--- kaynak tabanli ---"
sor "sube S-01 kitaplari" "http://127.0.0.1:8472/kitaplar?sube=S-01"
sor "U-1001 oduncleri"    "http://127.0.0.1:8472/oduncler?uye=U-1001"
sor "acik oduncler"       "http://127.0.0.1:8472/oduncler?durum=acik"
sor "adres kalibi sayisi" "http://127.0.0.1:8472/kaliplar"

kill $eylem $kaynak
```

```
eylem sunucusu 127.0.0.1:8471
kaynak sunucusu 127.0.0.1:8472
--- eylem tabanli ---
sube S-01 kitaplari    [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri       [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler          [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
yol sayisi             {"sayi":11}
--- kaynak tabanli ---
sube S-01 kitaplari    [{"isbn":"978-0201896831","ad":"Programlama Sanati","yazar":"Knuth","sube":"S-01"}]
U-1001 oduncleri       [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
acik oduncler          [{"id":1,"uye":"U-1001","isbn":"978-0262033848","iade":null}]
adres kalibi sayisi    {"sayi":4}
```

Yanıt gövdeleri satır satır aynı; sayılar aynı değil. On bir yola karşılık dört adres
kalıbı. Fark, kalıpların **istemci ihtiyacını değil kaynağı** adlandırmasından doğar.
"Şubedeki kitaplar" ve "açık ödünçler" ayrı kavramlar değildir; ikisi de var olan bir
koleksiyonun süzülmüş görünümüdür ve süzme ölçütü adresin sorgu bölümünde taşınır.

Sayının kendisi asıl sorun değildir. Asıl sorun, eylem tabanlı listenin **kapalı
olmamasıdır**. Bir ekran "geciken ödünçler" isterse liste on ikiye çıkar; başka bir ekran
"şubedeki geciken ödünçler" isterse on üçe. Kaynak tabanlı tarafta yeni bir sorgu ölçütü
eklenir, adres kümesi aynı kalır. Uç nokta sayısıyla ihtiyaç sayısı arasındaki bu
bağın kopması, kaynak modellemenin ölçülebilir çıktısıdır.

## İşlem de Bir Kaynağın Durum Değişimidir

Eylem tabanlı listede en dikkat çeken satır `/oduncIade` idi. İade bir fiildir, dolayısıyla
kaynak gibi görünmez. Ama iade edilen şey, sistemde zaten kimliği olan bir ödünç kaydıdır;
iade o kaydın `iade` alanının boş olmaktan çıkmasıdır. Yeni adres gerekmez, var olan
kaynağın durumu değişir.

Aşağıdaki servis bunu kütüphane şeması üstünde gösterir. Şema tüm ders boyunca aynıdır:
şube, kitap, üye ve ödünç tabloları.

```sql
-- sema.sql — kutuphane odunc servisinin cekirdek semasi
CREATE TABLE sube (
  kod TEXT PRIMARY KEY,
  ad  TEXT NOT NULL
);
CREATE TABLE kitap (
  isbn  TEXT PRIMARY KEY,
  ad    TEXT NOT NULL,
  yazar TEXT NOT NULL,
  yil   INTEGER NOT NULL,
  sube  TEXT NOT NULL REFERENCES sube(kod)
);
CREATE TABLE uye (
  kod  TEXT PRIMARY KEY,
  ad   TEXT NOT NULL,
  sube TEXT NOT NULL REFERENCES sube(kod)
);
CREATE TABLE odunc (
  id      INTEGER PRIMARY KEY,
  uye     TEXT NOT NULL REFERENCES uye(kod),
  isbn    TEXT NOT NULL REFERENCES kitap(isbn),
  verilis TEXT NOT NULL,
  iade    TEXT
);

INSERT INTO sube VALUES ('S-01','Merkez'), ('S-02','Yenisehir');
INSERT INTO kitap VALUES
  ('978-0201896831','Programlama Sanati','Knuth',1968,'S-01'),
  ('978-0262033848','Algoritmalara Giris','Cormen',1990,'S-02'),
  ('978-0131103627','C Programlama Dili','Ritchie',1978,'S-01');
INSERT INTO uye VALUES ('U-1001','Aylin Deren','S-01'), ('U-1002','Berk Tuna','S-02');
INSERT INTO odunc (uye, isbn, verilis, iade) VALUES
  ('U-1001','978-0262033848','2026-03-01',NULL),
  ('U-1001','978-0131103627','2026-02-10','2026-02-24'),
  ('U-1002','978-0201896831','2026-03-04',NULL);
```

```js
// kaynak-servisi.mjs — odunc bir kaynaktir; iade onun durum degisimidir
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));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const odunc = /^\/oduncler\/(\d+)$/.exec(yol);
  const durum = /^\/uyeler\/([\w-]+)\/durum$/.exec(yol);

  // Tekil odunc kaydi: bir tablo satirina karsilik gelir.
  if (odunc && istek.method === "GET") {
    const satir = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1]));
    return satir ? yanitla(yanit, 200, satir) : yanitla(yanit, 404, { hata: "odunc_yok" });
  }
  // Iade: yeni bir adres degil, var olan kaynagin durum degisimi.
  if (odunc && istek.method === "PATCH") {
    const govde = await govdeOku(istek);
    db.prepare("UPDATE odunc SET iade = ? WHERE id = ? AND iade IS NULL")
      .run(govde.iade, Number(odunc[1]));
    return yanitla(yanit, 200, db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(odunc[1])));
  }
  // Uye durumu: hicbir tabloda satiri yok, iki tablodan hesaplanir.
  if (durum && istek.method === "GET") {
    const sayilar = db.prepare(`
      SELECT COUNT(*) AS toplam, SUM(iade IS NULL) AS acik
      FROM odunc WHERE uye = ?`).get(durum[1]);
    const uye = db.prepare("SELECT ad, sube FROM uye WHERE kod = ?").get(durum[1]);
    if (!uye) return yanitla(yanit, 404, { hata: "uye_yok" });
    return yanitla(yanit, 200, {
      uye: durum[1], ad: uye.ad, sube: uye.sube,
      acikOdunc: sayilar.acik, toplamOdunc: sayilar.toplam,
      yeniOduncAlabilir: sayilar.acik < 2,
    });
  }
  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8473, "127.0.0.1", () => console.log("kaynak servisi 127.0.0.1:8473"));
```

```bash
# Semayi kurar, sunucuyu baslatir, odunc kaydini okur, iade eder, durumu sorar.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
node kaynak-servisi.mjs & sunucu=$!
sleep 0.4

curl -sS http://127.0.0.1:8473/oduncler/1; echo
curl -sS -X PATCH -H 'content-type: application/json' \
     -d '{"iade":"2026-03-12"}' http://127.0.0.1:8473/oduncler/1; echo
curl -sS http://127.0.0.1:8473/uyeler/U-1001/durum; echo
sqlite3 kutuphane.db "SELECT COUNT(*) || ' satir, acik: ' || SUM(iade IS NULL) FROM odunc;"

kill $sunucu
```

```
kaynak servisi 127.0.0.1:8473
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":null}
{"id":1,"uye":"U-1001","isbn":"978-0262033848","verilis":"2026-03-01","iade":"2026-03-12"}
{"uye":"U-1001","ad":"Aylin Deren","sube":"S-01","acikOdunc":0,"toplamOdunc":2,"yeniOduncAlabilir":true}
3 satir, acik: 1
```

Son satır önemlidir: iade yeni bir satır üretmedi, var olan satırın bir alanı doldu.
Tablodaki kayıt sayısı üçtür ve öyle kalır. İade için ayrı bir adres açmak, sistemde ayrı
bir şey yaratıldığı yanılsamasını verir; oysa yaratılan bir şey yoktur.

Bunun tersi de doğrudur ve abartıya kaçmamak gerekir. Bir geçişin kendi kuralları,
kendi yetkisi ve kendi kaydı varsa — ödüncün başka bir üyeye devri gibi — o geçişi kaynağa
bağlı ayrı bir adres olarak açmak savunulabilir. Ölçüt yine kimliktir: geçişin kendisi
sorgulanabilir bir kayıt bırakıyorsa kaynak olmayı hak eder, bırakmıyorsa alan değişimidir.

## Tablo ile Kaynak Aynı Şey Değil

Çıktının üçüncü satırı `/uyeler/U-1001/durum` adresine aittir ve bu kaynağın hiçbir tabloda
satırı yoktur. Açık ödünç sayısı, toplam ödünç sayısı ve yeni ödünç alma hakkı iki
tablodan hesaplanır. Yine de kaynaktır: kimliği sabittir, istemci ona doğrudan başvurur ve
sonucu tek bir gösterimdir. Kütüphane görevlisinin ekranı bu tek adresi çağırır; üç ayrı
sorgu yapıp kuralı istemcide uygulamaz.

Karşılık kurmak tek yönlü de değildir. Tek bir tablo birden çok kaynağı besleyebilir:
`odunc` tablosu hem ödünç kayıtları koleksiyonunu hem de bir üyenin ödünç geçmişini
besler. Kaynak modeli, veritabanı şemasının dışa yansıtılması değildir; şema depolama için
en iyi biçimi arar, kaynak modeli istemcinin başvurmak isteyeceği kavramları arar. İkisinin
çoğu zaman örtüşmesi, aynı şey oldukları anlamına gelmez — ve bu ayrım korunmazsa
veritabanında yapılan her yeniden düzenleme dışarıdaki sözleşmeyi kırar.

## Kimlik Seçiminin Bedeli

Kaynak kimliği olarak ne kullanılacağı bir tasarım kararıdır. İki seçenek vardır: Veri
Modelleme ve İlişkisel Kuram kursunda tanımlanan **doğal anahtar** — kitap için ISBN — ve
**vekil anahtar** — ödünç kaydı için üretilen sayı. Doğal anahtar okunabilir ve
anlamlıdır; vekil anahtar anlamsızdır ama değişmez.

Fark, veri düzeltildiğinde ortaya çıkar.

```bash
# Kaynak kimligi degisince ona yapilan basvurular ne olur?
rm -f kimlik.db
sqlite3 kimlik.db <<'SQL'
CREATE TABLE kitap (isbn TEXT PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE odunc (id INTEGER PRIMARY KEY, isbn TEXT NOT NULL REFERENCES kitap(isbn));
INSERT INTO kitap VALUES ('978-0262033848', 'Algoritmalara Giris');
INSERT INTO odunc (isbn) VALUES ('978-0262033848');
-- Katalog kaydindaki ISBN yanlis girilmis; duzeltiliyor.
UPDATE kitap SET isbn = '978-0262046305' WHERE isbn = '978-0262033848';
SELECT 'kitap tablosu : ' || isbn FROM kitap;
SELECT 'odunc tablosu : ' || isbn FROM odunc;
SELECT 'yetim kayit   : ' || COUNT(*)
  FROM odunc o LEFT JOIN kitap k ON k.isbn = o.isbn WHERE k.isbn IS NULL;
SQL
```

```
kitap tablosu : 978-0262046305
odunc tablosu : 978-0262033848
yetim kayit   : 1
```

Tek bir düzeltme bir yetim kayıt üretti. Bu motorda yabancı anahtar denetimi varsayılan
olarak kapalıdır ve `PRAGMA foreign_keys = ON` ile açılır; açık olsaydı güncelleme
reddedilir, düzeltme yapılamazdı. İki durumda da bedel vardır: ya başvuru kopar ya
düzeltme engellenir.

API tarafındaki karşılığı daha ağırdır, çünkü başvurular sistemin dışındadır.
`/kitaplar/978-0262033848` adresi bir kütüğe yazılmış, bir e-postaya konmuş, başka bir
servisin yapılandırmasına gömülmüş olabilir. Kimlik değişince bu adres artık bulunamaz.
Bu yüzden kaynak kimliği için ölçüt "anlamlı mı" değil, **"değişmeyeceğinden emin miyiz"**
sorusudur. Değişebilecek doğal anahtarlar kaynağın bir alanı olarak durur ve arama ölçütü
diye sunulur; kimlik için vekil anahtar kullanılır.

## Özet

- Kaynak, kimliği zaman içinde sabit kalan ve istemcinin doğrudan başvurduğu şeydir; her
  alan kavramı kaynak değildir, bazıları kaynağın alanı ya da servisin kuralıdır.
- Kaynağın kendisi soyuttur; istemciye giden gövde onun gösterimidir ve aynı kaynağın
  birden çok gösterimi olabilir.
- Eylem adlarıyla açılan uç noktaların sayısı istemci ihtiyaçlarıyla birlikte büyür; aynı
  ihtiyaçlar dört adres kalıbıyla karşılandığında bu bağ kopar.
- İade gibi geçişler yeni kaynak yaratmaz, var olan kaynağın durumunu değiştirir; kendi
  kaydını bırakan geçişler ise ayrı kaynak olmayı hak eder.
- Kaynak modeli veritabanı şemasının yansıması değildir: bir kaynak hiçbir tabloya
  karşılık gelmeyebilir, bir tablo birden çok kaynağı besleyebilir.
- Kimlik için ölçüt anlamlılık değil değişmezliktir; değişebilen doğal anahtarlar kimlik
  yerine alan olarak taşınır.

## Sonraki Adım

Kaynaklar belirlendi, ama adreslerinin nasıl yazılacağı henüz kararlaştırılmadı. Bu derste
`/kitaplar`, `/oduncler/1` ve `/uyeler/U-1001/durum` biçimleri sorgulanmadan kullanıldı;
oysa her biri bir seçimdi. Koleksiyon adı tekil mi çoğul mu olur, bir ödünç kaydına
`/oduncler/1` üzerinden mi yoksa `/uyeler/U-1001/oduncler/1` üzerinden mi başvurulur, iki
yol da açılırsa hangisi doğru sayılır? Sonraki ders yol düzenini bir eşleyici tablosu
kurarak ele alır ve iç içe adreslerin hangi noktada belirsizlik ürettiğini ölçer.
