Blok 4 · Ders 32
Yayına alma ve Blok 4 projesi: takım API'si
Express API'ni internete koyuyorsun: barındırma seçenekleri, ortam değişkenleri, loglar, alan adı ve DNS.
Bu dersin sonunda
- Sunucusuz ve sürekli çalışan barındırmayı karşılaştırıp uyumanın, soğuk başlangıcın ve geçici diskin API'ni nasıl etkilediğini açıklayabilirsin
- Takımlar ve şampiyonlar için CRUD uç noktaları, doğrulama, kayıt ara katmanı ve hata yakalayıcısı olan bir Express API'si kurup her ucunu terminalden deneyebilirsin
- Yayından önce temiz bir kopyada prova yapıp API'ni GitHub üzerinden Render'a yayına alabilirsin
- Ayarları barındırıcının ortam değişkenleri paneline girip canlıdaki hataları loglardan bulabilirsin
- Bir alan adını A ve CNAME kayıtlarıyla API'ne bağlamanın adımlarını anlatabilirsin
- README'ye uç nokta tablosu, örnek istek ve yanıtla bir API belgesi yazabilirsin
Bu sayfada
- Barındırma: API'n nerede yaşayacak?
- Proje planı: takım API'si
- Gelen veriyi denetlemek: dogrula.js
- sunucu.js: parça parça
- Projenin ilk kaydı
- Bütün uçları dene
- README: API belgesi
- GitHub'a gönder
- Yayın provası: temiz bir kopyada dene
- Render'a yayına almak
- Ortam değişkenlerini barındırıcıya girmek
- Loglara bakmak, canlıda hata ayıklamak
- Alan adı ve DNS kayıtları
- Görevler
- Kendini sına
- Kayıt noktası
Blok 4 boyunca sunucu yazdın: önce node:http ile, sonra Express ile. Veriyi dosyada tuttun, ayarları ortam değişkenlerine taşıdın, dış API'lere bağlandın, kimlik doğrulamayı öğrendin. Ama hepsi senin bilgisayarında çalıştı; bilgisayarın kapanınca API'n de kapandı.
Bu derste hepsini tek bir projede topluyorsun: takımları ve şampiyon havuzunu yöneten bir API yazacak, her ucunu deneyecek, belgesini yazacak ve yayına alacaksın: Ders 25'te statik bir siteyle yaptığını bu kez çalışan bir sunucuyla yapacaksın. Yolda barındırma türlerini, uyuyan sunucuları, loglardan hata bulmayı ve bir alan adını sunucuya bağlayan DNS kayıtlarını öğreneceksin.
Barındırma: API'n nerede yaşayacak?
Takım API'n şu an localhost:3000'de. Ders 18'den hatırla: localhost "bu bilgisayar" demek. Arkadaşın kendi bilgisayarında aynı adresi yazarsa kendi bilgisayarına bakar, senin API'ni bulamaz.
Bilgisayarını internete açmak da çözüm değil. Bilgisayar kapanınca, uyku moduna geçince ya da modem yeniden başlayınca API de kaybolur. Evdeki internetin IP adresi çoğu zaman sabit değildir ve modemler dışarıdan gelen bağlantıları varsayılan olarak geri çevirir. API'nin, internete hep bağlı ve hiç kapanmayan bir bilgisayarda çalışması gerekiyor.
GitHub Pages da bir barındırıcıydı, ama yalnız statik siteler için: dosyaları olduğu gibi gönderir, programını çalıştırmaz. Takım API'si ise her isteğin yanıtını o anda hesaplayan bir Node programı. Node programı çalıştıran barındırıcılar iki aileye ayrılır.
İki aile: sürekli çalışan ve sunucusuz
- Sürekli çalışan sunucu. Barındırıcı programını bir kez başlatır. Program, senin bilgisayarındaki
npm startgibi açık kalır ve gelen istekleri karşılar. Değişkenler istekler arasında yaşar. Render, Railway, Fly.io ve kiralık sanal sunucular (VPS) bu ailedendir. - Sunucusuz. Programı sen başlatmazsın. Bir istek gelince barındırıcı kodunun bir kopyasını çalıştırır, iş bitince kopyayı bir süre bekletir, sonra kapatır. Trafik artınca aynı anda birçok kopya açar. Vercel Functions, Netlify Functions, Cloudflare Workers ve AWS Lambda bu ailedendir.
| Sürekli çalışan | Sunucusuz | |
|---|---|---|
| Programın ömrü | Başlatılınca açık kalır | İstek gelince açılır, bir süre sonra kapanır |
| Bellekteki değişkenler | İstekler arasında yaşar | Kopyaya özeldir, kopya kapanınca gider |
| Diske yazmak | Olur, ama yeniden başlayınca silinebilir | Kod klasörü salt okunur, yalnız geçici /tmp |
| Express | Bilgisayarındaki gibi npm start ile çalışır | Barındırıcı uygulamayı bir fonksiyona çevirir |
| Ne için iyi? | Sürekli açık bağlantılar, arka plan işleri | Ani trafik, az kullanılan uç noktalar |
Ücretsiz seçenekler ve sınırları
Barındırıcıların ücretsiz planları sık değişir. Aşağıdaki bilgiler bu ders yazılırken, 12 Ekim 2026'da, iki barındırıcının resmî belgelerinden alındı. Karar vermeden önce güncel sayfalara bak.
| Render, ücretsiz web servisi | Vercel, Hobby planı | |
|---|---|---|
| Aile | sürekli çalışan | sunucusuz |
| Ücret | yok; çalışma alanı başına ayda 750 ücretsiz saat | yok; yalnız kişisel ve ticari olmayan kullanım |
| Uyuma | 15 dakika istek gelmezse uyur; uyanması yaklaşık 1 dakika | kopyalar trafiğe göre açılıp kapanır; uzun süre çağrılmayan fonksiyon arşivlenir, ilk çağrısı en az 1 saniye uzar |
| Disk | geçici: yeni sürümde, yeniden başlatmada ve uykuda sıfırlanır; kalıcı disk eklenemez | kod salt okunur; yalnız /tmp yazılabilir (en çok 500 MB) |
| Loglar | 7 gün saklanır | 1 saat saklanır |
| Not | Belgesi ücretsiz servisi deneme ve hobi projeleri için öneriyor, gerçek ürün için değil | Express uygulaması ayarsız çalışır ama giriş dosyasının adı index.js, app.js ya da server.js gibi belli adlardan biri olmalı |
Tablodaki çalışma alanı (workspace), Render'da servislerinin toplandığı hesap bölümü. Render'a kaydolunca ücretsiz Hobby çalışma alanıyla başlarsın; Pro gibi ücretli çalışma alanları daha uzun log süresi gibi ek özellikler verir.
Başka seçenekler de var. Örneğin Railway, 30 günlük bir deneme ve tek seferlik 5 dolarlık kredi veriyor; deneme bitince ayda 1 dolarlık krediyle ücretsiz plana geçiliyor.
750 saati hesapla: 31 gün çarpı 24 saat 744 saat eder. Yani tek bir servis ay boyunca hiç uyumasa bile saat yeter. İki servisi sürekli açık tutarsan saatler biter ve Render bütün ücretsiz servislerini ayın sonuna kadar askıya alır.
Bu derste Render'ı kullanacağız. Sebebi basit: Express API'n orada hiçbir değişiklik olmadan, bilgisayarındaki gibi npm start ile çalışıyor.
Uyuma ve soğuk başlangıç
Render'ın ücretsiz servisi 15 dakika boyunca hiç istek almazsa uyur: barındırıcı programını durdurur. Sonraki istek gelince programı yeniden başlatır. Bu yaklaşık bir dakika sürer ve o ilk istek bütün bu süreyi bekler.
Render, uyanma sırasında tarayıcıyla gelenlere bir "yükleniyor" sayfası gösterir. curl.exe ya da senin yazdığın bir program ise yanıtı bekler. Bekleyen taraf kısa bir zaman aşımı koyduysa (Ders 30'daki AbortSignal.timeout gibi) ilk istek hata alabilir. Ücretsiz bir API kullanan bir web sayfası yazıyorsan ilk istekte biraz sabırlı ol ya da kullanıcıya "sunucu uyanıyor" de.
Sunucusuz barındırıcılarda da soğuk başlangıç vardır, ama genelde daha kısadır: hiç açık kopya yoksa yenisi açılır, bu bir saniyenin altında da sürebilir, birkaç saniye de. Vercel'in belgesine göre iki hafta hiç çağrılmayan bir fonksiyon arşivlenir ve ilk çağrısı en az bir saniye daha uzun sürer.
Dosyaya yazdığın veri neden kaybolur?
Takım API'si verisini veri.json dosyasında tutacak. Ders 29'da dosyayı kalıcılık için kullanmıştın: bilgisayarında bu dosya sen silene kadar durur. Barındırıcıda ise durum farklı.
- Render: Belgesi açıkça söylüyor: ücretsiz servisin diski geçicidir; yüklenen dosyalar gibi yerel değişiklikler yeni sürümde, yeniden başlatmada ve uykuda kaybolur. Render ücretsiz servisi istediği an yeniden başlatabilir de. Ücretsiz serviste kalıcı disk de kullanamazsın. Yani yayındaki API'ne bir takım eklersin, 15 dakika kimse uğramaz, servis uyur; uyanınca
veri.jsonGitHub'daki hâline dönmüştür ve takımın yoktur. - Vercel: Kodun durduğu klasör salt okunurdur;
veri.json'a yazmaya kalkan kod hata alır. Yalnız/tmpklasörüne yazılabilir. Ama her kopyanın kendi/tmp'si vardır ve kopya kapanınca o da gider; A kopyasına yazdığını, B kopyasına düşen sonraki istek göremez.
Proje planı: takım API'si
API'n iki kaynağı yönetecek (Ders 26'daki REST mantığıyla): takımlar ve şampiyon havuzu. Her takımın adı, oyuncuları ve en çok beş şampiyonu var; şampiyonlar havuzdan seçilir.
| Yöntem | Yol | Ne yapar | Başarılı | Hatalı |
|---|---|---|---|---|
| GET | /api/takimlar | Bütün takımları listeler | 200 | |
| GET | /api/takimlar/:id | Tek takımı getirir | 200 | 404 |
| POST | /api/takimlar | Takım ekler | 201 | 400 |
| PATCH | /api/takimlar/:id | Takımın adını değiştirir | 200 | 400, 404 |
| DELETE | /api/takimlar/:id | Takımı siler | 204 | 404 |
| GET | /api/sampiyonlar | Şampiyon havuzunu listeler | 200 | |
| POST | /api/sampiyonlar | Havuza şampiyon ekler | 201 | 400 |
| PATCH | /api/sampiyonlar/:ad | Şampiyonun adını her yerde değiştirir | 200 | 400, 404 |
| DELETE | /api/sampiyonlar/:ad | Hiçbir takımda olmayan şampiyonu siler | 204 | 400, 404 |
Bunlara ek olarak her istek terminale bir satır yazacak, bozuk JSON 400, bilinmeyen adres 404, beklenmeyen hata 500 alacak ve tek bir ayarla bütün yazma işlemleri kapatılabilecek. Görevlerde de sağlık ucu, arama, sayfalama ve istek kimliği ekleyeceksin.
Proje bittiğinde klasörün şöyle görünecek:
takim-api/├── .env PORT ve öteki ayarlar (Git'e girmez)├── .gitignore├── package.json├── package-lock.json├── veri.json takımlar ve şampiyon havuzu├── dogrula.js gelen veriyi denetleyen fonksiyonlar├── sunucu.js Express uygulaması└── README.md API belgesiKurulum
Terminalde cd C:\kod ile kod klasörüne geç, mkdir takim-api ile yeni klasörü oluştur ve cd takim-api ile içine gir. Sonra package.json'ı oluştur:
PS C:\kod\takim-api> npm init -yWrote to C:\kod\takim-api\package.json: { "name": "takim-api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs"}Klasörü VS Code'da aç ve package.json'da üç şeyi değiştir: "type"'ı "module" yap, scripts'i aşağıdaki iki betikle değiştir ve en alta engines kısmını ekle. Sonra Express'i kur:
PS C:\kod\takim-api> npm install express added 65 packages, and audited 66 packages in 2s 28 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesKurulumdan sonra dosya şöyle olmalı:
{ "name": "takim-api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node sunucu.js", "dev": "node --watch --env-file=.env sunucu.js" }, "keywords": [], "author": "", "license": "ISC", "type": "module", "engines": { "node": "24.x" }, "dependencies": { "express": "^5.3.0" }}startyayında kullanılacak: yalnıznode sunucu.js. Barındırıcıda.envdosyası olmayacak, ayarları barındırıcı verecek. Bu yüzden--env-fileyalnız kendi bilgisayarında çalışandevbetiğinde.engines, projenin hangi Node sürümüyle çalıştığını söyler.24.x"24 ile başlayan herhangi bir sürüm" demek (Ders 14'teki anlamsal sürümleme). Render bu satıra bakıp sürümü seçer; belgesi aralığa bir üst sınır koymanı öneriyor,24.xde 25'i dışarıda bırakıyor.- Bu ders yazılırken Express'in son sürümü 5.3.0'dı. Sende daha yeni bir 5.x çıkabilir;
^işareti 5'ten başlayan yeni sürümlere izin verir.
Başlangıç verisi: veri.json
Proje klasöründe veri.json adında bir dosya aç ve şunu yaz. Dersin çıktıları bu veriyle alındı; kendi takımlarını daha sonra ekleyebilirsin.
{ "sonId": 6, "sampiyonlar": [ "Ahri", "Annie", "Brand", "Garen", "Jinx", "Lee Sin", "Lux", "Morgana", "Sona", "Teemo", "Thresh", "Vi", "Yasuo", "Zed" ], "takimlar": [ { "id": 1, "ad": "Gece Kurtları", "olusturulma": "2026-09-01T18:30:00.000Z", "oyuncular": [ { "kullaniciAdi": "deniz", "rol": "orta" }, { "kullaniciAdi": "ece", "rol": "destek" } ], "sampiyonlar": ["Ahri", "Lux", "Garen"] }, { "id": 2, "ad": "Mavi Ejderler", "olusturulma": "2026-09-02T09:15:00.000Z", "oyuncular": [ { "kullaniciAdi": "mert", "rol": "alt" }, { "kullaniciAdi": "selin", "rol": "orman" } ], "sampiyonlar": ["Jinx", "Thresh", "Lux"] }, { "id": 3, "ad": "Baron Avcıları", "olusturulma": "2026-09-03T20:00:00.000Z", "oyuncular": [ { "kullaniciAdi": "kaan", "rol": "orman" }, { "kullaniciAdi": "zeynep", "rol": "üst" } ], "sampiyonlar": ["Yasuo", "Lee Sin"] }, { "id": 4, "ad": "Kuzey Rüzgarı", "olusturulma": "2026-09-05T11:45:00.000Z", "oyuncular": [ { "kullaniciAdi": "arda", "rol": "üst" }, { "kullaniciAdi": "ipek", "rol": "orta" } ], "sampiyonlar": ["Annie", "Teemo"] }, { "id": 5, "ad": "Ateş Kuşları", "olusturulma": "2026-09-09T19:40:00.000Z", "oyuncular": [ { "kullaniciAdi": "cem", "rol": "orta" }, { "kullaniciAdi": "elif", "rol": "destek" } ], "sampiyonlar": ["Annie", "Brand"] }, { "id": 6, "ad": "Kurt Sürüsü", "olusturulma": "2026-09-12T21:10:00.000Z", "oyuncular": [ { "kullaniciAdi": "ozan", "rol": "üst" }, { "kullaniciAdi": "irem", "rol": "orman" } ], "sampiyonlar": ["Teemo", "Yasuo"] } ]}sonId: en son verilen takım numarası. Yeni takımsonId + 1alır. Bir takım silinse bile numarası yeniden kullanılmaz;/api/takimlar/4adresi hep aynı takımı göstermeli.sampiyonlar: havuz. Takımlar şampiyonlarını yalnız buradan seçebilir.olusturulma: takımın eklendiği an, Ders 12'deki JSON'da tarih tipi olmadığı için metin olarak. Biçimi birazdan göreceksin.
Ayarlar: .env ve .gitignore
API'nin iki ayarı .env'de duracak:
PORT=3000VERI_DOSYASI=veri.jsonnode_modules.envPORT sunucunun dinleyeceği kapı, VERI_DOSYASI verinin tutulacağı dosya. İkisi de gizli değil; yine de .env'yi Git'e koymuyoruz, çünkü ileride içine gizli bilgiler de girecek ve her bilgisayarın .env'si farklıdır. Kodda bir de üçüncü ayar olacak: SALT_OKUNUR. Onu .env'ye yazmıyoruz; yayındaki API'de, barındırıcının panelinden açıp kapatacağız.
Gelen veriyi denetlemek: dogrula.js
API'ne internetten herkes istek atabilecek. Bir kısmı yanlışlıkla, bir kısmı bilerek bozuk veri gönderecek: tek harflik bir ad, sayı yerine metin, havuzda olmayan bir şampiyon. Ders 13'teki kural burada da geçerli: dışarıdan gelen hiçbir veriye güvenme, önce denetle.
Denetimleri sunucu.js'ten ayrı bir dosyaya koyuyoruz. Bunun iki faydası var: route'lar kısa kalır, denetim fonksiyonları da sunucuyu açmadan tek başına denenebilir. Proje klasöründe dogrula.js adında bir dosya aç:
const ROLLER = ["üst", "orman", "orta", "alt", "destek"] export function adHatasi(ad) { if (typeof ad !== "string") { return "ad bir metin olmalı" } const uzunluk = ad.trim().length if (uzunluk < 2 || uzunluk > 40) { return "ad 2 ile 40 karakter arasında olmalı" } return null} export function takimHatasi(govde, havuz) { const adSorunu = adHatasi(govde?.ad) if (adSorunu) { return adSorunu } const oyuncular = govde.oyuncular ?? [] if (!Array.isArray(oyuncular) || oyuncular.length > 5) { return "oyuncular en çok 5 elemanlı bir dizi olmalı" } for (const oyuncu of oyuncular) { if (typeof oyuncu?.kullaniciAdi !== "string" || oyuncu.kullaniciAdi.trim() === "") { return "her oyuncunun kullaniciAdi alanı dolu olmalı" } if (!ROLLER.includes(oyuncu.rol)) { return "rol şunlardan biri olmalı: " + ROLLER.join(", ") } } const sampiyonlar = govde.sampiyonlar ?? [] if (!Array.isArray(sampiyonlar) || sampiyonlar.length > 5) { return "sampiyonlar en çok 5 elemanlı bir dizi olmalı" } const gorulen = [] for (const ad of sampiyonlar) { if (!havuz.includes(ad)) { return `${ad} şampiyon havuzunda yok` } if (gorulen.includes(ad)) { return `${ad} listede iki kez geçiyor` } gorulen.push(ad) } return null} export function sampiyonHatasi(ad, havuz) { if (typeof ad !== "string") { return "ad bir metin olmalı" } const temiz = ad.trim() if (temiz.length < 2 || temiz.length > 20) { return "şampiyon adı 2 ile 20 karakter arasında olmalı" } if (havuz.some((mevcut) => mevcut.toLowerCase() === temiz.toLowerCase())) { return `${temiz} zaten havuzda` } return null}Üç fonksiyonun da sözü aynı: sorun varsa açıklamasını, yoksa null döndür. Böylece route'ta tek bir if yeter.
adHatasi, adın metin olup olmadığına ve baştaki, sondaki boşluklar atıldıktan sonra 2 ile 40 karakter arasında olup olmadığına bakar." G "görünüşte uzun, ama içinde tek harf var.takimHatasi(govde, havuz)bütün gövdeyi denetler.govde?.ad'deki?.(Ders 12) önemli: Express 5'te gövdesiz bir isteğinreq.body'siundefinedolur.govde.adyazsaydık API, gövdesiz bir POST'taTypeErrorile çökerdi.Array.isArray(deger)"bu bir dizi mi?" sorusunun cevabını verir.typeofburada işe yaramaz, çünkü dizilere de"object"der.- Oyuncu ya da şampiyon gönderilmezse
?? []boş listeyi varsayar; ikisi de isteğe bağlı. - Tekrarı
gorulendizisiyle yakalıyoruz: her şampiyon önce bu diziye bakılarak denetlenir, sonra eklenir. sampiyonHatasiaynı şampiyonun ikinci kez eklenmesini, büyük-küçük harfe bakmadan engeller:" lux "ile"Lux"aynı şampiyon. Şampiyon adları İngilizce olduğu için düztoLowerCaseyeterli (Ders 9'daki kural).
Fonksiyonları sunucuya bağlamadan önce sınırlarda dene. Aynı klasörde dogrula-deneme.js adında geçici bir dosya aç. "A".repeat(40) "A harfini 40 kez yan yana yaz" demek; adı tam sınırda denemenin kısa yolu:
import { adHatasi, takimHatasi, sampiyonHatasi } from "./dogrula.js" const havuz = ["Ahri", "Lux", "Zed"] console.log(adHatasi("G"))console.log(adHatasi("Gö"))console.log(adHatasi(" G "))console.log(adHatasi("A".repeat(40)))console.log(adHatasi("A".repeat(41)))console.log(adHatasi(123))console.log(takimHatasi(undefined, havuz))console.log(takimHatasi({ ad: "Gece Kurtları", sampiyonlar: ["Ahri", "Lux"] }, havuz))console.log(takimHatasi({ ad: "Gece Kurtları", sampiyonlar: ["Ahri", "Ahri"] }, havuz))console.log(takimHatasi({ ad: "Gece Kurtları", oyuncular: [{ kullaniciAdi: "deniz", rol: "jungle" }] }, havuz))console.log(sampiyonHatasi(" lux ", havuz))PS C:\kod\takim-api> node dogrula-deneme.jsad 2 ile 40 karakter arasında olmalınullad 2 ile 40 karakter arasında olmalınullad 2 ile 40 karakter arasında olmalıad bir metin olmalıad bir metin olmalınullAhri listede iki kez geçiyorrol şunlardan biri olmalı: üst, orman, orta, alt, desteklux zaten havuzda2 karakter ve 40 karakter geçti, 1 ve 41 geçmedi: sınırlar doğru yerde. Gövdesiz istek (undefined) çökmek yerine anlaşılır bir mesaj aldı. Denedikten sonra dogrula-deneme.js'i sil; projenin parçası değil.
sunucu.js: parça parça
Şimdi asıl dosya. sunucu.js'i yukarıdan aşağı, parça parça yazacağız; her parçanın başındaki satır numarası dosyadaki yerini gösteriyor. Parçaları sırayla alt alta yazınca dosyanın tamamı ortaya çıkar.
Ayarlar ve veri dosyası
import express from "express"import { readFile, writeFile } from "node:fs/promises"import { resolve } from "node:path"import { adHatasi, takimHatasi, sampiyonHatasi } from "./dogrula.js" const app = express()const port = process.env.PORT ?? 3000const veriDosyasi = resolve(import.meta.dirname, process.env.VERI_DOSYASI ?? "veri.json")const saltOkunur = process.env.SALT_OKUNUR === "evet" async function veriyiOku() { const metin = await readFile(veriDosyasi, "utf8") return JSON.parse(metin)} async function veriyiYaz(veri) { await writeFile(veriDosyasi, JSON.stringify(veri, null, 2))}- Bütün ayarlar en üstte, program başlarken bir kez okunuyor. Yayında bir ayarı değiştirirsen programın yeniden başlaması gerekir; barındırıcı bunu kendisi yapacak.
- Ortam değişkenleri her zaman metindir.
SALT_OKUNURiçin=== "evet"diye soruyoruz; değişken hiç yoksa sonuçfalseolur. import.meta.dirnamebu dosyanın bulunduğu klasör:C:\kod\takim-api(Ders 29). Oradapath.joinile birleştirmiştin; buradanode:path'inresolvefonksiyonunu kullanıyoruz, çünkü bir farkı var: ad zaten tam bir yolsa (D:\yedek\veri.jsongibi) klasörü atıp onu olduğu gibi kullanır. BöyleceVERI_DOSYASIayarına hemveri.jsonhem tam bir yol yazılabilir.veriyiOkuher istekte dosyayı baştan okur,veriyiYazbütün veriyi geri yazar.JSON.stringify(veri, null, 2)dosyayı 2 boşluk girintiyle, okunur biçimde yazar (Ders 12).
Neden düz "veri.json" değil de tam yol? Ders 29'dan hatırla: göreli bir yol, dosyanın değil, programın çalışma klasörünün içinde aranır. Kendi bilgisayarında hep proje klasöründen başlattığın için fark etmezsin; başka bir klasörden başlatan ya da projeyi bir alt klasörde tutan bir barındırıcı ise dosyayı bulamaz:
Error: ENOENT: no such file or directory, open 'C:\kod\veri.json'Ne zaman çıkar? Kodda göreli bir dosya yolu varken (readFile("veri.json")) programı proje klasörünün dışından başlatınca. Aşağıda sunucuyu C:\kod klasöründeyken node takim-api\sunucu.js ile başlattık, sonra bir istek attık. Sunucu açıldı ama her istek 500 aldı; sunucunun terminali sebebini yazdı:
PS C:\kod> node takim-api\sunucu.jsTakım API'si çalışıyor: http://localhost:3000Veri dosyası: veri.jsonError: ENOENT: no such file or directory, open 'C:\kod\veri.json' at async open (node:internal/fs/promises:1345:25) at async readFile (node:internal/fs/promises:1996:14) at async veriyiOku (file:///C:/kod/takim-api/sunucu.js:12:19) at async file:///C:/kod/takim-api/sunucu.js:39:18 { errno: -4058, code: 'ENOENT', syscall: 'open', path: 'C:\\kod\\veri.json'}GET /api/takimlar 500 5 msENOENT "böyle bir dosya ya da klasör yok" (error: no entry) demek. Yol satırına dikkat et: Node dosyayı C:\kod\veri.json'da aradı, oysa dosya C:\kod\takim-api\'de. İkinci satırdaki Veri dosyası: veri.json de ipucuydu: yol tam değildi.
Nasıl düzelir? Dosya yolunu programın klasörüne göre değil, kod dosyasının klasörüne göre kur: resolve(import.meta.dirname, "veri.json"). Yukarıdaki kod böyle; çıktıyı göstermek için onu bir kopyada bilerek göreli yola çevirdik.
Kayıt ara katmanı
Yayındaki API'nin başında oturmayacaksın. Kim, ne zaman, hangi adrese istek attı, ne cevap aldı, ne kadar sürdü: bunları sonradan okuyabilmek için her isteğin bir satırlık kaydını tutman gerekir.
Render gelen istekleri ancak ücretli Pro ve üstü çalışma alanlarında kendiliğinden loglar; ücretsiz Hobby çalışma alanında bunu kendin yapman gerekir. İşi, Ders 28'de yazdığın kayıt tutucunun aynısı görecek. sunucu.js'e devam et:
app.use((req, res, next) => { const baslangic = Date.now() res.on("finish", () => { const sure = Date.now() - baslangic console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${sure} ms`) }) next()})Ders 28'den hatırla:
Date.now()o anın milisaniye cinsinden değeri (Ders 23). İstek gelince bir kez okuyoruz, yanıt bitince bir kez daha; fark, isteğin ne kadar sürdüğü.res.on("finish", fonksiyon): "yanıt gönderilip bitince bu fonksiyonu çalıştır." Durum kodunu ancak yanıt bitince bilebildiğimiz için satırı orada yazıyoruz.next()isteği sıradaki ara katmana geçirir. Unutursan ısınmadaki ikinci sorunun belirtisi çıkar: hiçbir istek yanıt almaz.
Tek yenilik req.url yerine req.originalUrl: isteğin geldiği tam adres, sorgu kısmı (?ara=kurt gibi) dahil. Bu projede ikisi aynı şeyi yazar. Express bazı durumlarda req.url'yi yolda değiştirir, originalUrl ise hep ilk hâli tutar; loga yazılacak adres için daha güvenli seçim bu.
Bu ara katman en üstte, bütün route'lardan önce durmalı. Express ara katmanları yazıldıkları sırayla çalıştırır; route'lardan sonraya koyarsan, yanıtı zaten gönderilmiş istekler ona hiç uğramaz.
Salt okunur anahtarı ve JSON gövdesi
app.use((req, res, next) => { if (saltOkunur && req.method !== "GET") { return res.status(503).json({ hata: "API şu an salt okunur" }) } next()}) app.use(express.json())Bu, Ders 28'deki bakım modu görevinin aynısı. SALT_OKUNUR=evet ise GET dışındaki bütün istekler, route'lara hiç ulaşmadan 503 Service Unavailable ("hizmet şu an kullanılamıyor") alır.
Tek fark anahtarın yeri: Ders 28'de kodda bir const'tu, burada bir ortam değişkeni. Böylece bakım yaparken ya da herkese açık bir deneme API'sini bozulmaktan korumak isterken, yayındaki API'de kodu değiştirmeden yazmayı kapatırsın. express.json() bu kapıdan sonra geliyor; reddedeceğimiz bir isteğin gövdesini okumaya gerek yok.
Bir isteğin sunucu.js içindeki bütün yolu şöyle:
Takım uç noktaları
Beş route da aynı düzende: dosyayı oku, bul ya da denetle, değiştir, dosyaya yaz, yanıtla. Ders 28'deki CRUD'un aynısı; tek fark verinin dizide değil, dosyada durması.
app.get("/api/takimlar", async (req, res) => { const veri = await veriyiOku() res.json(veri.takimlar)}) app.get("/api/takimlar/:id", async (req, res) => { const veri = await veriyiOku() const takim = veri.takimlar.find((t) => t.id === Number(req.params.id)) if (!takim) { return res.status(404).json({ hata: "Takım bulunamadı" }) } res.json(takim)}) app.post("/api/takimlar", async (req, res) => { const veri = await veriyiOku() const hata = takimHatasi(req.body, veri.sampiyonlar) if (hata) { return res.status(400).json({ hata }) } const oyuncular = req.body.oyuncular ?? [] veri.sonId = veri.sonId + 1 const takim = { id: veri.sonId, ad: req.body.ad.trim(), olusturulma: new Date().toISOString(), oyuncular: oyuncular.map((o) => ({ kullaniciAdi: o.kullaniciAdi.trim(), rol: o.rol })), sampiyonlar: req.body.sampiyonlar ?? [] } veri.takimlar.push(takim) await veriyiYaz(veri) res.status(201).json(takim)})req.params.idher zaman metindir ("3");Numberile sayıya çeviripidile karşılaştırıyoruz."abc"gibi bir değerNaNolur, hiçbir takımla eşleşmez ve404döner.return res.status(...): hata yanıtından sonra fonksiyonu bitiriyoruz ki ikinci bir yanıt yazılmasın (Ders 27'dekiERR_HTTP_HEADERS_SENT).{ hata },{ hata: hata }'nın kısa yazımı: alanın adı, değerini tutan kutunun adıyla aynıysa bir kez yazman yeter. Ders 12'dekiconst { hp } = ahriparçalamasının tersi gibi düşün: orada alan kutuya açılıyordu, burada kutu aynı adlı alana giriyor.new Date().toISOString()şu anı dünya saatiyle (UTC) ve her dilde aynı okunan bir biçimde yazar:2026-10-11T18:17:42.853Z. SondakiZ, saatin UTC olduğunu söyler; Türkiye saati bunun 3 saat ilerisidir.- Oyuncuları olduğu gibi kaydetmiyoruz:
mapher oyuncudan yalnızkullaniciAdiverolalanlarını alıyor. Biri gövdeye"yonetici": truegibi fazladan bir alan koysa bile dosyaya girmez.
app.patch("/api/takimlar/:id", async (req, res) => { const veri = await veriyiOku() const takim = veri.takimlar.find((t) => t.id === Number(req.params.id)) if (!takim) { return res.status(404).json({ hata: "Takım bulunamadı" }) } const hata = adHatasi(req.body?.ad) if (hata) { return res.status(400).json({ hata }) } takim.ad = req.body.ad.trim() await veriyiYaz(veri) res.json(takim)}) app.delete("/api/takimlar/:id", async (req, res) => { const veri = await veriyiOku() const id = Number(req.params.id) if (!veri.takimlar.some((t) => t.id === id)) { return res.status(404).json({ hata: "Takım bulunamadı" }) } veri.takimlar = veri.takimlar.filter((t) => t.id !== id) await veriyiYaz(veri) res.status(204).end()})PATCH takımın yalnız adını değiştirir. takim, veri.takimlar dizisinin içindeki objenin kendisi (Ders 12'deki referans); onun adını değiştirince veri de değişmiş olur, veriyiYaz da bu hâli yazar. DELETE önce some ile "bu numarada bir takım var mı?" diye sorar (Ders 11), sonra Ders 28'deki gibi filter ile o takımı dışarıda bırakan yeni bir dizi kurar ve gövdesiz 204 döner.
Şampiyon uç noktaları
app.get("/api/sampiyonlar", async (req, res) => { const veri = await veriyiOku() res.json(veri.sampiyonlar)}) app.post("/api/sampiyonlar", async (req, res) => { const veri = await veriyiOku() const hata = sampiyonHatasi(req.body?.ad, veri.sampiyonlar) if (hata) { return res.status(400).json({ hata }) } const ad = req.body.ad.trim() veri.sampiyonlar.push(ad) veri.sampiyonlar.sort((a, b) => a.localeCompare(b)) await veriyiYaz(veri) res.status(201).json({ ad })})Havuz alfabetik kalsın diye her eklemeden sonra localeCompare ile sıralıyoruz (Ders 11).
Şampiyonun adını değiştirmek daha ilginç. Ad yalnız havuzda değil, onu seçen her takımın içinde de yazılı. Bir yerde değiştirip ötekileri unutursan takımlar havuzda olmayan bir şampiyonu göstermeye başlar. Bu yüzden route bütün takımları tek tek dolaşıyor:
app.patch("/api/sampiyonlar/:ad", async (req, res) => { const veri = await veriyiOku() const eskiAd = req.params.ad if (!veri.sampiyonlar.includes(eskiAd)) { return res.status(404).json({ hata: "Şampiyon bulunamadı" }) } const hata = sampiyonHatasi(req.body?.ad, veri.sampiyonlar) if (hata) { return res.status(400).json({ hata }) } const yeniAd = req.body.ad.trim() const degistir = (ad) => (ad === eskiAd ? yeniAd : ad) veri.sampiyonlar = veri.sampiyonlar.map(degistir).sort((a, b) => a.localeCompare(b)) let takimSayisi = 0 for (const takim of veri.takimlar) { if (takim.sampiyonlar.includes(eskiAd)) { takim.sampiyonlar = takim.sampiyonlar.map(degistir) takimSayisi = takimSayisi + 1 } } await veriyiYaz(veri) res.json({ eskiAd, yeniAd, takimSayisi })}) app.delete("/api/sampiyonlar/:ad", async (req, res) => { const veri = await veriyiOku() const ad = req.params.ad if (!veri.sampiyonlar.includes(ad)) { return res.status(404).json({ hata: "Şampiyon bulunamadı" }) } const kullananlar = veri.takimlar.filter((t) => t.sampiyonlar.includes(ad)) if (kullananlar.length > 0) { return res.status(400).json({ hata: `${ad} ${kullananlar.length} takımda kullanılıyor, silinemez` }) } veri.sampiyonlar = veri.sampiyonlar.filter((s) => s !== ad) await veriyiYaz(veri) res.status(204).end()})degistirküçük bir yardımcı: eski ada denk gelen adı yenisiyle, ötekileri olduğu gibi döndürür. Hem havuzda hem takımlarda aynı fonksiyonu kullanıyoruz.- Silme, şampiyonu kullanan bir takım varsa reddediliyor; yoksa bir takım havuzda olmayan bir şampiyonla kalırdı.
404 ve hata yakalayıcı
app.use((req, res) => { res.status(404).json({ hata: "Böyle bir uç nokta yok" })}) app.use((hata, req, res, next) => { if (hata.type === "entity.parse.failed") { return res.status(400).json({ hata: "Gövde geçerli bir JSON değil" }) } console.error(hata) res.status(500).json({ hata: "Sunucuda bir hata oldu" })})- İlki, hiçbir route'a uymayan istekleri yakalar. Route'ların hepsinden sonra durduğu için oraya ancak eşleşmeyen istekler düşer.
- İkincisi hata yakalayıcı: dört parametresi olduğu için Express onu sıradan bir ara katman değil, hata yakalayıcı sayar. Express 5'te
asyncbir route'un içinde fırlayan hata da kendiliğinden buraya gelir. express.json()bozuk bir gövdeyle karşılaşıncatypealanı"entity.parse.failed"olan bir hata fırlatır. Bu bizim hatamız değil, istemcinin; o yüzden400.- Geri kalan her şey beklenmeyen bir hatadır. Ayrıntıyı istemciye değil, loga yazıyoruz: hatanın içinde dosya yolları ve kodundan parçalar olabilir, bunları dışarıya göstermek saldırgana ipucu verir. İstemci yalnız "bir hata oldu" duyar.
Sunucuyu başlatmak
app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Takım API'si çalışıyor: http://localhost:${port}`) console.log(`Veri dosyası: ${veriDosyasi}`) if (saltOkunur) { console.log("Salt okunur: GET dışındaki istekler 503 alacak") }})Başlarken ayarları loga yazmak iyi bir alışkanlık: yayındaki programın hangi ayarlarla çalıştığını tek bakışta görürsün. Gizli bilgileri (şifre, anahtar) ise asla loga yazma; loglar da başkalarının gözüne girebilir.
Ne zaman çıkar? Express 5'te app.listen'a verilen fonksiyon, port açılamazsa da çağrılır; hata ilk parametre olarak gelir. Hatayı hiç denetlemezsen program "çalışıyor" yazar ve sessizce kapanır. En sık sebep: sunucu başka bir terminalde zaten çalışıyor.
app.listen(port, () => { console.log(`Takım API'si çalışıyor: http://localhost:${port}`) console.log(`Veri dosyası: ${veriDosyasi}`)})PS C:\kod\takim-api> node --env-file=.env sunucu.jsTakım API'si çalışıyor: http://localhost:3000Veri dosyası: C:\kod\takim-api\veri.jsonKomut istemi hemen geri geldi: program kapandı. İstekler hâlâ birinci terminaldeki eski sunucuya gidiyor; yaptığın değişiklikleri göremez, "kodum çalışmıyor" sanırsın.
Nasıl düzelir? Fonksiyona hata parametresini ver ve varsa fırlat. Yukarıdaki doğru sürüm aynı durumda gerçek sebebi söyler:
PS C:\kod\takim-api> node --env-file=.env sunucu.jsfile:///C:/kod/takim-api/sunucu.js:169 throw hata ^ Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:2324:16) ... code: 'EADDRINUSE', errno: -4091, syscall: 'listen', address: '::', port: 3000} Node.js v24.20.0EADDRINUSE'u Ders 27'den tanıyorsun: kapıyı başka bir program tutuyor. Eski sunucuyu kendi terminalinde Ctrl+C ile durdur.
Dosya tamam. Sunucuyu npm run dev ile başlat:
import express from "express"import { readFile, writeFile } from "node:fs/promises"import { resolve } from "node:path"import { adHatasi, takimHatasi, sampiyonHatasi } from "./dogrula.js" const app = express()const port = process.env.PORT ?? 3000const veriDosyasi = resolve(import.meta.dirname, process.env.VERI_DOSYASI ?? "veri.json")const saltOkunur = process.env.SALT_OKUNUR === "evet" async function veriyiOku() { const metin = await readFile(veriDosyasi, "utf8") return JSON.parse(metin)}PS C:\kod\takim-api> npm run dev > takim-api@1.0.0 dev> node --watch --env-file=.env sunucu.js Takım API'si çalışıyor: http://localhost:3000Veri dosyası: C:\kod\takim-api\veri.jsonSunucu açık ve hangi dosyayı kullandığını söylüyor. node_modules klasörü de gezginde görünür; onu listeye yazmadık.
Projenin ilk kaydı
Uçları denemeden önce projeyi Git'e kaydet. Denemeler veri.json'u değiştirecek; kayıtta temiz hâli dursun ki sonra ona dönebilesin. Komutları ikinci bir terminalde yaz (terminal panelindeki +), sunucu birincide açık kalsın:
Bütün uçları dene
Sunucu birinci terminalde açık; istekleri ikincisinden at. Yazma isteklerinde Invoke-RestMethod kullanıyoruz: JSON'u PowerShell'in kendi metni olarak gönderdiği için Ders 26'daki tırnak sorunu çıkmaz. -ContentType'ın sonundaki charset=utf-8'i unutma; Türkçe harfler onunla doğru gider.
Okumak
PS C:\kod\takim-api> (Invoke-RestMethod http://localhost:3000/api/takimlar) | Select-Object id, ad id ad-- -- 1 Gece Kurtları 2 Mavi Ejderler 3 Baron Avcıları 4 Kuzey Rüzgarı 5 Ateş Kuşları 6 Kurt Sürüsü PS C:\kod\takim-api> curl.exe http://localhost:3000/api/takimlar/3{"id":3,"ad":"Baron Avcıları","olusturulma":"2026-09-03T20:00:00.000Z","oyuncular":[{"kullaniciAdi":"kaan","rol":"orman"},{"kullaniciAdi":"zeynep","rol":"üst"}],"sampiyonlar":["Yasuo","Lee Sin"]}PS C:\kod\takim-api> curl.exe -i http://localhost:3000/api/takimlar/99HTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 29ETag: W/"1d-dROtwCE5hGBhioJB963Igac7VbI"Date: Sun, 11 Oct 2026 18:17:42 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Takım bulunamadı"}Select-Object id, ad her takımdan yalnız iki alanı tablo yapar. Baştaki parantezlere dikkat; neden gerektiğini aşağıdaki hata kartında göreceksin. Content-Length: 29 gövdenin bayt sayısı: {"hata":"Takım bulunamadı"} 27 karakter, ama içindeki iki ı harfinin her biri UTF-8'de 2 bayt tuttuğu için 29 bayt.
Select-Object boş bir tablo veriyorNe zaman çıkar? Windows PowerShell 5.1'de Invoke-RestMethod'un döndürdüğü bir JSON dizisini parantezsiz olarak | ile başka bir komuta verince. Aşağıdaki komut, yukarıdakinin parantezsiz hâli: Invoke-RestMethod http://localhost:3000/api/takimlar | Select-Object id, ad.
PS C:\kod\takim-api> Invoke-RestMethod http://localhost:3000/api/takimlar | Select-Object id, ad id ad-- -- Hata mesajı yok, ama altı takımın yerinde tek bir boş satır var. PowerShell 5.1, gelen diziyi borunun öbür ucuna altı ayrı takım olarak değil, tek parça olarak geçirir. Select-Object o tek parçada id ve ad arar, bulamaz.
Nasıl düzelir? Komutu parantez içine al: (Invoke-RestMethod ...) | Select-Object id, ad. Parantez, önce komutun bitmesini ve sonucun elemanlarına ayrılmasını sağlar. PowerShell 7'de bu sorun yok.
Takım eklemek, değiştirmek, silmek
PS C:\kod\takim-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/takimlar -ContentType "application/json; charset=utf-8" -Body '{"ad": "Gölge Takımı", "oyuncular": [{"kullaniciAdi": "baris", "rol": "orman"}], "sampiyonlar": ["Vi", "Zed"]}' id : 7ad : Gölge Takımıolusturulma : 2026-10-11T18:17:42.853Zoyuncular : {@{kullaniciAdi=baris; rol=orman}}sampiyonlar : {Vi, Zed} PS C:\kod\takim-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/takimlar -ContentType "application/json; charset=utf-8" -Body '{"ad": "G"}'Invoke-RestMethod : {"hata":"ad 2 ile 40 karakter arasında olmalı"}At line:1 char:1...PS C:\kod\takim-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/takimlar -ContentType "application/json; charset=utf-8" -Body '{"ad": "Gölge Ordusu", "sampiyonlar": ["Zed", "Zedd"]}'Invoke-RestMethod : {"hata":"Zedd şampiyon havuzunda yok"}At line:1 char:1...Yeni takım sonId'den bir fazlasını, 7'yi aldı. olusturulma sende o anın saati olur. Invoke-RestMethod, 4xx alınca hata fırlatır ve gövdeyi hata mesajı olarak gösterir (Ders 26); bu yüzden API'nin açıklaması ilk satırda okunuyor.
Kısalttığımız satırlar (At line:1 char:1 ile başlayan blok) PowerShell'in hatanın komutun neresinde olduğunu gösteren kısmı. Sonra takımın adını değiştir ve onu sil:
PS C:\kod\takim-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/takimlar/7 -ContentType "application/json; charset=utf-8" -Body '{"ad": "Gölge Ordusu"}' id : 7ad : Gölge Ordusuolusturulma : 2026-10-11T18:17:42.853Zoyuncular : {@{kullaniciAdi=baris; rol=orman}}sampiyonlar : {Vi, Zed} PS C:\kod\takim-api> curl.exe -i -X DELETE http://localhost:3000/api/takimlar/7HTTP/1.1 204 No ContentX-Powered-By: ExpressDate: Sun, 11 Oct 2026 18:17:42 GMTConnection: keep-aliveKeep-Alive: timeout=5 204 No Content'te gövde yok, Content-Type başlığı da yok. Aynı komutu bir kez daha çalıştırırsan takım artık olmadığı için 404 alırsın.
Şampiyon havuzu
PS C:\kod\takim-api> curl.exe http://localhost:3000/api/sampiyonlar["Ahri","Annie","Brand","Garen","Jinx","Lee Sin","Lux","Morgana","Sona","Teemo","Thresh","Vi","Yasuo","Zed"]PS C:\kod\takim-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"ad": "Karma"}' ad--Karma PS C:\kod\takim-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"ad": "lux "}'Invoke-RestMethod : {"hata":"lux zaten havuzda"}At line:1 char:1...PS C:\kod\takim-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/sampiyonlar/Lux -ContentType "application/json; charset=utf-8" -Body '{"ad": "Luxanna"}' eskiAd yeniAd takimSayisi------ ------ -----------Lux Luxanna 2 PS C:\kod\takim-api> curl.exe http://localhost:3000/api/takimlar/1{"id":1,"ad":"Gece Kurtları","olusturulma":"2026-09-01T18:30:00.000Z","oyuncular":[{"kullaniciAdi":"deniz","rol":"orta"},{"kullaniciAdi":"ece","rol":"destek"}],"sampiyonlar":["Ahri","Luxanna","Garen"]}PS C:\kod\takim-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/Annie{"hata":"Annie 2 takımda kullanılıyor, silinemez"}PS C:\kod\takim-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/Morgana"lux "ile"Lux"aynı sayıldı; havuza ikinci bir Lux girmedi.- Lux'ın adı iki takımda birden değişti; 1 numaralı takımın listesinde artık
Luxannavar. - Annie iki takımda kullanıldığı için silinmedi. Morgana'yı hiçbir takım seçmemişti; silindi ve
204geldiği içincurl.exehiçbir şey yazmadı.
Bozuk istekler
PS C:\kod\takim-api> curl.exe --json "{" http://localhost:3000/api/takimlar{"hata":"Gövde geçerli bir JSON değil"}PS C:\kod\takim-api> curl.exe http://localhost:3000/api/esyalar{"hata":"Böyle bir uç nokta yok"}İkisinde de sunucu çökmedi ve istemciye ne yanlış olduğunu söyledi.
Sunucunun logu
Şimdi birinci terminale bak. Attığın her istek için bir satır var:
GET /api/takimlar 200 6 msGET /api/takimlar/3 200 1 msGET /api/takimlar/99 404 1 msPOST /api/takimlar 201 12 msPOST /api/takimlar 400 2 msPOST /api/takimlar 400 1 msPATCH /api/takimlar/7 200 2 msDELETE /api/takimlar/7 204 2 msGET /api/sampiyonlar 200 2 msPOST /api/sampiyonlar 201 14 msPOST /api/sampiyonlar 400 2 msPATCH /api/sampiyonlar/Lux 200 4 msGET /api/takimlar/1 200 2 msDELETE /api/sampiyonlar/Annie 400 4 msDELETE /api/sampiyonlar/Morgana 204 2 msPOST /api/takimlar 400 2 msGET /api/esyalar 404 1 msInvoke-RestMethod'un sana göstermediği durum kodları burada: takım eklemek 201, silmek 204 döndü. Son sütundaki süreler sende farklı çıkar; aynı istek bile her seferinde birkaç milisaniye oynar. Yayında bu satırlar Render'ın log sayfasına akacak.
Veriyi eski hâline döndür
Denemeler veri.json'u değiştirdi. Git ne değiştiğini biliyor:
PS C:\kod\takim-api> git statusOn branch masterChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: veri.json no changes added to commit (use "git add" and/or "git commit -a")PS C:\kod\takim-api> git restore veri.jsonPS C:\kod\takim-api> git statusOn branch masternothing to commit, working tree cleanGit'in ipucundaki git restore <file> komutu, dosyayı son kayıttaki hâline döndürür. Bu kayıtta olmayan değişiklikleri geri dönüşsüz siler; kendi yazdığın bir kod dosyasında kullanmadan önce iki kez düşün. Burada istediğimiz tam bu: deneme verisi gitti, Karma havuzdan çıktı, Lux'ın adı geri geldi. Sunucu her istekte dosyayı baştan okuduğu için yeniden başlatmana gerek yok.
README: API belgesi
API'ni kullanacak kişi kodunu okumayacak; hangi adrese, hangi gövdeyle istek atacağını ve ne cevap alacağını bilmek isteyecek. Altı ay sonra sen de kendi API'nin ayrıntılarını unutmuş olacaksın. Bu bilgiyi README'ye yaz. Bir API belgesinin olmazsa olmazları: kurulum, ayarlar, uç nokta tablosu, en az bir örnek istek ve yanıt, hata biçimi ve bilinen sınırlar.
Proje klasöründe README.md adında bir dosya aç. Kopyalamak yerine kendi cümlelerinle yaz; aşağıdaki bir örnek:
# Takım API'si Takımları, oyuncularını ve şampiyon havuzunu tutan küçük bir REST API. Express 5 ile yazıldı, veriyi `veri.json` dosyasında saklar. Canlı adres: https://takim-api-XXXX.onrender.com (ücretsiz plan: 15 dakika istek gelmezse uyur, ilk istek yaklaşık bir dakika sürebilir) ## Kurulum 1. `npm install`2. `.env` dosyası oluştur (aşağıdaki ayarlar)3. `npm run dev` ## Ayarlar | Değişken | Varsayılan | Ne işe yarar ||---|---|---|| `PORT` | `3000` | Sunucunun dinlediği port. Barındırıcı kendisi verir. || `VERI_DOSYASI` | `veri.json` | Verinin tutulduğu dosya || `SALT_OKUNUR` | (boş) | `evet` yazılırsa GET dışındaki istekler 503 alır | ## Uç noktalar | Yöntem | Yol | Ne yapar | Başarılı yanıt ||---|---|---|---|| GET | `/api/takimlar` | Bütün takımları listeler | 200 || GET | `/api/takimlar/:id` | Tek takımı getirir | 200 || POST | `/api/takimlar` | Yeni takım ekler | 201 || PATCH | `/api/takimlar/:id` | Takımın adını değiştirir | 200 || DELETE | `/api/takimlar/:id` | Takımı siler | 204 || GET | `/api/sampiyonlar` | Şampiyon havuzunu listeler | 200 || POST | `/api/sampiyonlar` | Havuza şampiyon ekler | 201 || PATCH | `/api/sampiyonlar/:ad` | Şampiyonun adını her yerde değiştirir | 200 || DELETE | `/api/sampiyonlar/:ad` | Hiçbir takımda olmayan şampiyonu siler | 204 | ## Örnek: takım eklemek İstek: `POST /api/takimlar`, başlık `Content-Type: application/json` ```json{ "ad": "Gölge Takımı", "oyuncular": [{ "kullaniciAdi": "baris", "rol": "orman" }], "sampiyonlar": ["Vi", "Zed"]}``` Yanıt: `201 Created` ```json{ "id": 7, "ad": "Gölge Takımı", "olusturulma": "2026-10-11T18:17:42.853Z", "oyuncular": [{ "kullaniciAdi": "baris", "rol": "orman" }], "sampiyonlar": ["Vi", "Zed"]}``` Kurallar: `ad` 2–40 karakter; en çok 5 oyuncu, `rol` şunlardan biri: `üst`, `orman`, `orta`, `alt`, `destek`; en çok 5 şampiyon, hepsi havuzda olmalı ve tekrar etmemeli. ## Hatalar Hata yanıtlarının hepsi aynı biçimdedir: `{ "hata": "açıklama" }` | Kod | Ne zaman ||---|---|| 400 | Gövde bozuk ya da kurala uymuyor || 404 | Takım, şampiyon ya da adres yok || 500 | Sunucuda beklenmeyen hata (loglara bak) || 503 | API salt okunur kipte | ## Sınırlar Veri bir JSON dosyasında durur. Ücretsiz barındırıcıda sunucu yeniden başlayınca ya da yeni sürüm yüklenince dosyadaki değişiklikler kaybolur; kalıcı veri için veritabanına geçilecek.- Uç nokta tablosu belgenin kalbi: her satırda yöntem, yol, ne yaptığı ve başarılı yanıtın kodu.
:idve:ad"buraya bir değer gelir" demek; Express'teki yazımın aynısı. - Örnek istek ve yanıt, kuralları tek tek anlatmaktan çoğu zaman daha öğreticidir. Örnekteki yanıtı uydurma, gerçekten aldığın yanıtı koy; biz yukarıdaki POST'un çıktısını kullandık.
- Hata biçimi: API'yi kullanan program hataları tek bir yoldan okuyabilmeli. Bizimkinde her hata
{ "hata": "..." }. - Sınırlar: API'nin yapamadıklarını saklama; verinin kaybolabileceğini bilen biri ona göre davranır.
- Canlı adresi yayına aldıktan sonra yazacaksın.
XXXXşimdilik yer tutucu.
VS Code'da Ctrl+Shift+V README'nin önizlemesini açar; tabloların düzgün göründüğüne bak. Sonra kaydet:
PS C:\kod\takim-api> git add .PS C:\kod\takim-api> git commit -m "README: API belgesi"[master e060511] README: API belgesi 1 file changed, 74 insertions(+) create mode 100644 README.mdGitHub'a gönder
Render kodunu GitHub'dan alacak. GitHub'da takim-api adında yeni bir depo aç; README, .gitignore ve lisans seçeneklerini boş bırak, bunlar sende zaten var (Ders 25'teki gibi). Herkese açık (Public) ya da gizli (Private) olabilir; Render ikisinden de yayına alır. Sonra GitHub'ın gösterdiği komutlarla gönder; KULLANICI yerine kendi kullanıcı adını yaz:
PS C:\kod\takim-api> git branch -M mainPS C:\kod\takim-api> git remote add origin https://github.com/KULLANICI/takim-api.gitPS C:\kod\takim-api> git push -u origin main# İlk gönderimde tarayıcıda GitHub'a giriş isteyebilir (Ders 7'deki gibi).GitHub'daki depo sayfasını yenile: altı dosyan ve README'nin önizlemesi orada olmalı. .env olmamalı. Varsa hemen sil, .gitignore'u düzelt ve içinde gizli bir şey varsa değiştir (Ders 29'daki kural: sızan anahtar yenilenir).
Yayın provası: temiz bir kopyada dene
Render senin klasörünü görmeyecek. GitHub'daki deponun temiz bir kopyasını alacak, içinde npm install ve npm start çalıştıracak. Kopyada node_modules olmayacak, .env olmayacak, Git'e girmemiş hiçbir dosya olmayacak. "Bende çalışıyordu" hatalarının çoğu bu farktan çıkar.
Bu farkı yayından önce kendi bilgisayarında yakalayabilirsin: depoyu başka bir klasöre klonla ve Render'ın yapacağını sırayla yap. Önce sunucunu birinci terminalde Ctrl+C ile durdur. Sonra ikinci terminalde:
PS C:\kod\takim-api> cd ..PS C:\kod> git clone takim-api provaCloning into 'prova'...done.PS C:\kod> cd provaPS C:\kod\prova> npm start > takim-api@1.0.0 start> node sunucu.js node:internal/modules/package_json_reader:301 throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null); ^ Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from C:\kod\prova\sunucu.js at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9) ... code: 'ERR_MODULE_NOT_FOUND'} Node.js v24.20.0git clone takim-api prova yerel depoyu prova adlı yeni bir klasöre kopyaladı; GitHub'dan klonlamak da aynı sonucu verirdi. İlk deneme bilerek erken: paketleri kurmadan başlatırsan ne olacağını gör.
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from C:\kod\prova\sunucu.jsNe zaman çıkar? Paketler kurulmamışken programı başlatınca. node_modules Git'e girmez (Ders 14); klonlanan her kopyada paketler yeniden kurulmalıdır. Barındırıcıda bu hatayı görüyorsan derleme komutu (Build Command) boştur ya da npm install çalışmamıştır. Bir başka sebep: paketi npm install express ile değil de başka bir projeden kopyalayarak getirdiysen package.json'daki dependencies'te adı yoktur.
Nasıl düzelir? Önce npm install çalıştır; package.json'ın dependencies kısmında express'in yazdığından emin ol. Render'da Build Command'ın npm install olduğuna bak.
Şimdi Render'ın sırasıyla devam et: önce paketleri kur, sonra Render'ın vereceği portla başlat. Render, programına PORT ortam değişkeninde 10000 verir. $env:PORT = "10000", bu değişkeni yalnız bu terminal için tanımlar:
PS C:\kod\prova> npm install added 65 packages, and audited 66 packages in 2s 28 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesPS C:\kod\prova> $env:PORT = "10000"PS C:\kod\prova> npm start > takim-api@1.0.0 start> node sunucu.js Takım API'si çalışıyor: http://localhost:10000Veri dosyası: C:\kod\prova\veri.jsonProgram .env olmadan da açıldı ve portu ortamdan aldı. Üçüncü bir terminal açıp dene:
PS C:\kod> curl.exe http://localhost:10000/api/takimlar/1{"id":1,"ad":"Gece Kurtları","olusturulma":"2026-09-01T18:30:00.000Z","oyuncular":[{"kullaniciAdi":"deniz","rol":"orta"},{"kullaniciAdi":"ece","rol":"destek"}],"sampiyonlar":["Ahri","Lux","Garen"]}Prova geçti: temiz kopya kuruluyor, başlıyor ve yanıt veriyor. Sunucuyu Ctrl+C ile durdur ve prova terminalini kapat (terminal listesindeki çöp kutusu simgesi). $env:PORT yalnız o terminalde tanımlıydı, onunla birlikte gider. C:\kod\prova klasörünü de Dosya Gezgini'nden silebilirsin.
Provada çıkabilecek bir hata daha var. Bir dosyayı Git'e eklemeyi unuttuysan klon onu içermez:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\kod\prova\dogrula.js' imported from C:\kod\prova\sunucu.jsNe zaman çıkar? import ile çağrılan bir dosya klonda yokken. İki sebebi var. Birincisi, dosya hiç commit'lenmemiştir: senin klasöründe durur, GitHub'da yoktur.
İkincisi daha sinsi: büyük-küçük harf. Windows'ta Dogrula.js ile dogrula.js aynı dosya sayılır, Render'ın kullandığı Linux'ta ise iki ayrı dosyadır. import ... from "./Dogrula.js" yazıp dosyayı dogrula.js adıyla kaydettiysen kendi bilgisayarında çalışır, Render'da bu hatayı verir. Aşağıdaki çıktıyı provada dogrula.js'in adını geçici olarak değiştirip aldık:
PS C:\kod\prova> node sunucu.jsnode:internal/modules/esm/resolve:272 throw new ERR_MODULE_NOT_FOUND( ^ Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\kod\prova\dogrula.js' imported from C:\kod\prova\sunucu.js at finalizeResolution (node:internal/modules/esm/resolve:272:11) ... code: 'ERR_MODULE_NOT_FOUND', url: 'file:///C:/kod/prova/dogrula.js'} Node.js v24.20.0Render'ın logunda yol /opt/render/project/src/dogrula.js gibi bir Linux yolu olur.
Nasıl düzelir? git status ile commit'lenmemiş dosya var mı bak; varsa git add, git commit, git push. Harf sorunu için import satırındaki adı dosyanın gerçek adıyla harfi harfine aynı yaz. Bu kurstaki gibi dosya adlarını hep küçük harfle yazmak bu hatayı baştan önler.
Render'a yayına almak
Prova geçtiyse yayına hazırsın. Aşağıdaki adımlar Ekim 2026'daki Render belgelerine göre yazıldı.
- Hesap aç. render.com'da Sign Up'a bas ve GitHub hesabınla kaydol. GitHub izin sorduğunda Render'a ya bütün depolarına ya da yalnız
takim-api'ye erişim ver. Render bazı hesaplardan doğrulama için kart bilgisi isteyebiliyor; ücretsiz planda karttan para çekilmez, doğrulama için açılan küçük geçici tutar iade edilir. Kart veremiyorsan dersin geri kalanını okuyarak geç: prova, yayının yapacağının aynısını zaten gösterdi. - Yeni servis. Panelde sağ üstteki + New menüsünden Web Service'i seç. Depo listesinden
takim-api'yi bul ve Connect'e bas. - Formu doldur. Aşağıdaki şekildeki değerleri gir. Bölge (Region) olarak Türkiye'ye en yakın olan Frankfurt'u seç; bölge sonradan değiştirilemez. Instance Type (bazı ekranlarda Compute) olarak Free'yi seç.
- Ortam değişkenleri. Şimdilik boş bırak;
PORT'u Render kendisi verir. Birazdan bir değişken ekleyeceğiz. - Yayınla. Formun altındaki Deploy (bazı ekranlarda Create Web Service) düğmesine bas. Render seni servisin sayfasına götürür; Deploys bölümünde ilk yayının çıktısını satır satır görürsün.
- Bekle ve aç. Yayının durumu Live olunca servis sayfasının üstünde adresin görünür: servisin adından türeyen,
onrender.comile biten bir adres. Ad doluysa Render sonuna birkaç karakter ekleyebilir; bu derste onutakim-api-XXXX.onrender.comdiye yazıyoruz.
- Root Directory boş kalıyor, çünkü
package.jsondeponun kökünde. Proje deponun bir alt klasöründe olsaydı onun adını yazardın. - Build Command paketleri kurar: provadaki
npm install. Start Command programı başlatır: provadakinpm start. - Node sürümünü
package.json'dakienginessöylüyor; Render'ın yeni servisler için varsayılanı da bu ders yazılırken 24'tü.
Render'ın yayın sırasında yaptıkları:
git push'tan yayına giden yol. Deploys sayfası Render'ın adımlarını, Logs sayfası programının yazdıklarını gösterir.Dördüncü adım önemli: Render programının bir portu dinlemeye başladığını görmeden yayını tamamlanmış saymaz. Bunun için programın 0.0.0.0 adresinde, yani bilgisayarın bütün ağ adreslerinde dinlemesi gerekir.
app.listen(port)'a adres vermezsen zaten bütün adresleri dinler: yukarıdaki EADDRINUSE mesajındaki address: '::' bunu söylüyordu. app.listen(port, "localhost") yazsaydın program yalnız kendi içinden gelen istekleri kabul eder, Render ona ulaşamazdı. Belgesine göre programın başka bir portu dinlerse Render çoğu zaman onu da bulur; yine de doğru yol process.env.PORT'u kullanmak.
Yayın bitince canlı adresini dene. XXXX yerine kendi adresini yaz:
curl.exe https://takim-api-XXXX.onrender.com/api/takimlar/1Yanıtın, provadaki curl.exe çıktısıyla harfi harfine aynı olmalı: yayındaki program, provada çalıştırdığın programın ta kendisi. README'deki XXXX'i de gerçek adresinle değiştir.
Sonra curl.exe -i ile başlıklara da bak. Express'in başlıklarına Render'ınkiler eklenir. Aşağıdaki çıktı, Render'ın belgelerindeki örnek Express uygulamasından (express.onrender.com); sayfanın HTML gövdesini kestik:
PS C:\kod> curl.exe -i https://express.onrender.comHTTP/1.1 200 OKDate: Mon, 12 Oct 2026 00:16:32 GMTContent-Type: text/html; charset=utf-8Transfer-Encoding: chunkedConnection: keep-alivecf-cache-status: DYNAMICetag: W/"68e-VaTXc2PRFPGatiGUiAAczSjuh6s"rndr-id: b11a01d4-7bed-4b33Server: cloudflarevary: Accept-Encodingx-powered-by: Expressx-render-origin-server: RenderCF-RAY: a491ea9c0fa33101-ISTalt-svc: h3=":443"; ma=86400x-powered-by: Expressveetagprogramın kendi başlıkları; yolda adları küçük harfe çevrilmiş. Ders 26'dan hatırla: başlık adlarında büyük-küçük harf fark etmez.x-render-origin-server: Renderverndr-idRender'ın;rndr-id'yi birazdan istek kimliği konusunda tekrar göreceksin.Server: cloudflare,CF-RAYvecf-cache-status, Render'ın önünde duran Cloudflare ağından geliyor.CF-RAY'in sonundakiIST, isteğin İstanbul'daki bir Cloudflare sunucusundan geçtiğini söylüyor; sende başka bir kod çıkabilir.- Windows'un
curl.exe'si HTTP'nin yeni sürümü HTTP/2'yi konuşmadığı için yanıt satırıhttpsadreste deHTTP/1.1ile başlar.
Ortam değişkenlerini barındırıcıya girmek
Kendi bilgisayarında ayarlar .env'den geliyordu. Render'da .env yok; değişkenleri ya Render kendisi verir ya da sen panelden girersin.
| Değişken | Kim verir? | Değeri |
|---|---|---|
PORT | Render | 10000. Sen girme; kodun zaten process.env.PORT'u okuyor. |
NODE_ENV | Render (Node servislerinde, çalışırken) | production |
RENDER_GIT_COMMIT | Render | yayındaki commit'in kimliği |
VERI_DOSYASI | sen (isteğe bağlı) | girmezsen kod veri.json'u kullanır |
SALT_OKUNUR | sen (isteğe bağlı) | evet ise yazma kapalı |
| Gizli bilgiler | sen | ileride veritabanı şifresi gibi değerler buraya girecek |
Panelin nasıl çalıştığını zararsız bir ayarla dene: yayındaki API'ni salt okunur yap.
- Render'da servisinin sayfasını aç ve soldaki menüden Environment'a gir.
- Environment Variables bölümünde + Add Environment Variable'a bas. Key olarak
SALT_OKUNUR, Value olarakevetyaz. - Kaydet düğmesinin açılır menüsünden Save and deploy'u seç. Render mevcut derlemeyi yeni değişkenle yeniden başlatır.
Kaydetme menüsündeki üç seçenek:
- Save, rebuild, and deploy: baştan derler (
npm installdahil) ve yayınlar. - Save and deploy: var olan derlemeyi yeni değişkenlerle yeniden başlatır. Yalnız bir değişken değiştirdiysen yeterli ve daha hızlı.
- Save only: kaydeder ama yayınlamaz. Değişken bir sonraki yayına kadar etkisizdir; bu yüzden "değiştirdim ama bir şey olmadı" sorusunun en sık cevabı budur.
Yayın bitince bir yazma isteği dene. Aşağıdaki çıktıları provadaki gibi kendi bilgisayarımızda, aynı değişkenle aldık; canlı adreste gövde aynıdır:
PS C:\kod\takim-api> curl.exe -i -X DELETE https://takim-api-XXXX.onrender.com/api/takimlar/1HTTP/1.1 503 Service UnavailableX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 33ETag: W/"21-+2OXKDulu7CLsBVwT/Z62s6iFwg"Date: Sun, 11 Oct 2026 18:18:56 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"API şu an salt okunur"}PS C:\kod\takim-api> curl.exe https://takim-api-XXXX.onrender.com/api/takimlar/1{"id":1,"ad":"Gece Kurtları","olusturulma":"2026-09-01T18:30:00.000Z","oyuncular":[{"kullaniciAdi":"deniz","rol":"orta"},{"kullaniciAdi":"ece","rol":"destek"}],"sampiyonlar":["Ahri","Lux","Garen"]}Silme reddedildi, okuma çalışıyor. Logun başında da program durumu söyler:
Takım API'si çalışıyor: http://localhost:10000Veri dosyası: C:\kod\takim-api\veri.jsonSalt okunur: GET dışındaki istekler 503 alacakDELETE /api/takimlar/1 503 4 msGET /api/takimlar/1 200 4 msRender'da Veri dosyası satırında Windows yolu yerine bir Linux yolu görürsün; localhost:10000 da Render'ın içinden bakınca doğru. İstersen değişkeni silip (Environment sayfasında satırın yanındaki çöp kutusu) yazmayı yeniden aç.
Loglara bakmak, canlıda hata ayıklamak
Yayındaki API'de hata olduğunda terminal yok, hata ayıklayıcı yok. Elinde iki şey var: isteği kendin tekrarlamak ve logları okumak. Render'da iki ayrı log yeri var:
- Deploys: her yayının kendi çıktısı. Klonlama,
npm install'ın yazdıkları venpm start'ın ilk satırları burada. Yayın başarısız olduysa sebebi buradadır. - Logs: çalışan programın yazdıkları: kayıt ara katmanının satırları,
console.errorile yazılan hatalar. Program çalıştığı sürece akar.
- Sayfa varsayılan olarak son bir saati gösterir; sağ üstteki menüden aralığı değiştirirsin. Live tail satırları geldikleri anda akıtır: bir isteği atıp sonucunu canlı izlemek için.
- Arama kutusuna yazdığın metin (
500, bir yol, bir istek kimliği) yalnız onu içeren satırları bırakır. - Ücretsiz Hobby çalışma alanında loglar 7 gün saklanır. Pro ve üstü çalışma alanlarında Render her isteği kendisi de loglar; Hobby'de o satırlar yok, bu yüzden kendi kayıt ara katmanını yazdın.
Hangi istek hangi satır? İstek kimliği
Aynı anda onlarca istek gelirken log satırları birbirine karışır. Bir kullanıcı "biraz önce hata aldım" dediğinde onun isteğini bulmak zordur. Çözüm, her isteğe kısa bir ad vermek:
Ders 26'da httpbin'in yanıtlarında X-Amzn-Trace-Id diye bir başlık görmüştün; Amazon'un istek kimliği. Render da aynı fikri kullanıyor: canlı adresin başlıklarında gördüğün rndr-id Render'ın istek kimliği. Render onu hem yanıtla istemciye hem de isteğin başlığında senin programına gönderir; Pro çalışma alanlarındaki istek loglarının her satırında da aynı kimlik requestID adıyla durur. Kendi API'ne istek kimliği eklemek dördüncü görevin.
Canlıda hata ayıklamanın altı adımı
- Belirtiyi tekrarla. Hatayı kendin üret:
curl.exe -iile aynı isteği canlı adrese at. Durum kodunu ve gövdeyi gör. - Logu bul. Logs sayfasında isteğin zamanına, yoluna, durum koduna ya da istek kimliğine göre ara.
- İlk hata satırını oku. Hatanın türü ve mesajı, altındaki
atsatırlarında da kendi dosyan ve satır numarası (Ders 13'teki yığın izi). - Yerelde tekrarla. Aynı isteği
localhost'a at. Hata yerelde de çıkıyorsa her zamanki gibi düzelt. Çıkmıyorsa fark ortamdadır: eksik bir ortam değişkeni, Git'e girmemiş bir dosya, büyük-küçük harf, başka bir Node sürümü. - Düzelt ve gönder. Commit,
git push; Deploys sayfasında yeni yayının Live olmasını bekle. - Canlıda doğrula. İlk adımdaki isteği tekrarla. İlk görevdeki sağlık ucu, yayındaki commit'in hangisi olduğunu da söyleyecek.
Yayında en sık karşılaşacağın üç durum:
npm error Missing script: "start"Ne zaman çıkar? Render'ın Start Command'ı npm start iken package.json'da start betiği yoksa. Yayın Deploys sayfasında başarısız olur, sitende hâlâ eski sürüm (ya da hiçbir şey) vardır. Kendi bilgisayarında npm run dev kullandığın için betiğin eksik olduğunu fark etmemişsindir:
PS C:\kod\takim-api> npm startnpm error Missing script: "start"npm errornpm error Did you mean one of these?npm error npm star # Mark your favorite packagesnpm error npm stars # View packages marked as favoritesnpm errornpm error To see a list of scripts, run:npm error npm runnpm error A complete log of this run can be found in: C:\Users\Deniz\AppData\Local\npm-cache\_logs\2026-10-11T18_13_01_490Z-debug-0.lognpm benzer adlı komutlar öneriyor (star, stars); onların bu işle ilgisi yok.
Nasıl düzelir? package.json'daki scripts kısmına "start": "node sunucu.js" satırını ekle, commit'le ve gönder. Provayı yapsaydın bu hatayı yayından önce görürdün.
SyntaxError: Unexpected end of JSON input (her istek 500 alıyor)Ne zaman çıkar? veri.json boş ya da yarım kalmışsa. writeFile önce dosyanın içini boşaltır, sonra yeni içeriği yazar; program tam o arada kapanırsa (barındırıcı yeniden başlatırken, elektrik kesilince) dosya boş kalabilir. Bu, Ders 29'daki "yarım kalan dosya" tehlikesi.
Elle düzenlerken bir virgül unutmak da mesajı biraz farklı bir SyntaxError verir. Sonuçta API'nin bütün uçları 500 döner. Kullanıcı yalnız {"hata":"Sunucuda bir hata oldu"} görür; sebep logdadır:
SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>) at veriyiOku (file:///C:/kod/takim-api/sunucu.js:13:17) at async file:///C:/kod/takim-api/sunucu.js:39:18GET /api/takimlar 500 8 msYığın izini aşağıdan yukarı oku: liste route'u (sunucu.js:39) veriyiOku'yu çağırmış, o da 13. satırda JSON.parse'ta düşmüş. "Unexpected end of JSON input", "JSON beklenmedik yerde bitti" demek; boş bir metni çözmeye çalışınca da bu mesaj çıkar.
Nasıl düzelir? Kendi bilgisayarında git restore veri.json. Render'da servisi yeniden başlatmak dosyayı GitHub'daki hâline döndürür. Yarım kalmayı Ders 29'daki geçici dosya ve rename önler. Hem yarım kalmayı hem aynı anda yazmayı kendisi yöneten ve yeniden başlatmada kaybolmayan çözüm ise bir veritabanı; Blok 5'in konusu.
Üçüncü durumda hiçbir hata mesajı yok:
Alan adı ve DNS kayıtları
Adresin şu an takim-api-XXXX.onrender.com. Çalışır, ama akılda kalmaz ve Render'ın adını taşır. Kendi alan adın varsa API'ni api.alanadin.com gibi bir adrese bağlayabilirsin.
Ders 18'den hatırla: DNS internetin rehberi; tarayıcı bir adı önce DNS'e sorar, aldığı numaraya bağlanır. Rehberdeki her satıra DNS kaydı denir. Her kaydın bir türü, bir adı ve bir değeri vardır.
Alan adı Ders 18'de gördüğün gibi yıllık ücretle kiralanır. Bu bölümü uygulamak için bir alan adı satın alman gerekir; almayacaksan okuyup geç, onrender.com adresin ücretsiz ve yeterli. Örneklerde, tam bu iş için ayrılmış example.com'u kullanıyoruz; sen kendi alan adını yazacaksın.
api.example.com'u sorar. DNS sunucusu CNAME'i kendisi izler ve tek cevapta hem CNAME'i hem A kaydını verir; tarayıcı bulduğu numaraya, yani Render'a bağlanır.Render aynı IP adresinde binlerce servisi barındırır. Bir isteği hangi servise vereceğini, tarayıcının Ders 26'da gördüğün Host başlığına yazdığı addan anlar. Bu yüzden DNS kaydı tek başına yetmez: adı Render'a da söylemen gerekir. Birazdan göreceğin Custom Domains adımı bu işe yarar.
Rehbere kendin de sorabilirsin. nslookup CNAME'leri Aliases (takma adlar) satırında gösterir:
PS C:\kod> nslookup www.github.comServer: dns.googleAddress: 2001:4860:4860::8888 Non-authoritative answer:Name: github.comAddress: 140.82.121.4Aliases: www.github.com PS C:\kod> nslookup express.onrender.comServer: dns.googleAddress: 2001:4860:4860::8888 Non-authoritative answer:Name: gcp-us-west1-1.origin.onrender.com.cdn.cloudflare.netAddresses: 216.24.57.16 216.24.57.18Aliases: express.onrender.com gcp-us-west1-1.origin.onrender.comwww.github.combir CNAME:github.com'a yönlendiriyor, onun A kaydı da bir IP adresi veriyor.express.onrender.com, Render'ın belgelerindeki örnek Express uygulaması. Onun adı da iki CNAME'den geçip Render'ın216.24.57.xadreslerine varıyor. Seninonrender.comadresin de böyle çalışır.- Numaralar ve sunucu adları zamanla değişebilir; sende farklı çıkarsa sorun değil.
Her kaydın bir de TTL'si vardır: Ders 30'da önbellek için gördüğün yaşam süresi. DNS'te bu, cevabı alanların onu kaç saniye hatırlayabileceğini söyler. Bir kaydı değiştirdiğinde eski cevabı hatırlayanlar TTL dolana kadar eskisini kullanır. "DNS'i değiştirdim ama hâlâ eski yere gidiyor" şikâyetinin sebebi budur. PowerShell'in Resolve-DnsName komutu kaydın türünü ve TTL'sini de gösterir:
PS C:\kod> Resolve-DnsName www.github.com -Type CNAME Name Type TTL Section NameHost---- ---- --- ------- --------www.github.com CNAME 3600 Answer github.com API'ni kendi alan adına bağlamak
Render ücretsiz servislerde de alan adı bağlamaya izin veriyor; ücretsiz çalışma alanında iki alan adı dahil. Sertifikayı da kendisi alıp yeniliyor, yani adresin https ile çalışıyor.
- Render'da servisinin Settings sayfasında Custom Domains bölümüne in ve + Add Custom Domain'e bas.
api.example.comyaz (sen kendi alan adını) ve kaydet. - Alan adını aldığın şirketin DNS paneline gir ve aşağıdaki tablodaki CNAME kaydını ekle.
- Render'a dön ve alan adının yanındaki Verify düğmesine bas. Doğrulama başarısız olursa kaydın yayılmamış olabilir; birkaç dakika bekleyip yeniden dene.
- Doğrulanınca Render sertifikayı kendisi alır. Birkaç dakika sonra
https://api.example.com/api/takimlarçalışır;httpile gelenlerhttps'e yönlendirilir.
DNS panelinde kayıtlar çoğu zaman şöyle bir tabloyla girilir:
| Tür | Ad | Değer | Ne için |
|---|---|---|---|
| CNAME | api | takim-api-XXXX.onrender.com | api.example.com alt alan adı |
| ANAME ya da ALIAS | @ | takim-api-XXXX.onrender.com | kök ad example.com, şirketin bu türü destekliyorsa |
| A | @ | 216.24.57.1 | kök ad, ANAME/ALIAS yoksa |
- Ad sütunu alan adının başına eklenecek kısımdır:
apiyazarsan kayıtapi.example.comiçin olur.@, kök adın kendisi demek. - Kök ada (
example.com) CNAME konamaz; DNS'in kuralı bu. Bu yüzden kök ad için ya şirketlerin CNAME benzeri ANAME/ALIAS kaydı ya da Render'ın belgesinde verdiği IP adresine bir A kaydı kullanılır. Kök adı eklersen Renderwwwalt adını da kendisi ekler ve köke yönlendirir. - Render'ın belgesi, alan adında
AAAA(IPv6) kaydı varsa silmeni istiyor: Render IPv4 kullanıyor. - Bilgiler Ekim 2026'daki Render belgelerinden; IP adresini ve adımları bağlamadan önce güncel belgeden doğrula.
curl: (6) Could not resolve host: api.example.comNe zaman çıkar? DNS'te o ad için hiçbir kayıt bulunamayınca: kaydı daha eklemediysen, adı yanlış yazdıysan (api yerine apı) ya da kayıt henüz yayılmadıysa.
PS C:\kod> curl.exe https://api.example.com/api/takimlarcurl: (6) Could not resolve host: api.example.comPS C:\kod> Invoke-RestMethod https://api.example.com/api/takimlarInvoke-RestMethod : Uzak ad çözülemedi: 'api.example.com'...İngilizce Windows'ta ikinci mesaj The remote name could not be resolved: 'api.example.com' olur. 6 curl'ün "adı çözemedim" hata numarası; Ders 27'deki 7 ise "adı buldum ama bağlanamadım" demekti.
Nasıl düzelir? nslookup api.example.com ile rehbere sor. Cevapta adres yoksa kayıt yoktur: DNS panelindeki Ad ve Değer'i harf harf denetle. Kayıt yeni eklendiyse birkaç dakika (eski bir cevap hatırlanıyorsa TTL kadar) bekle.
Görevler
Görevler projeyi büyütüyor; her biri bir öncekinin bıraktığı yerden devam ediyor. Sunucu npm run dev ile açık kalsın, istekleri ikinci terminalden at. Her görevden sonra commit'leyip gönderirsen Render onu da yayına alır. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Sağlık ucu
GET /saglik adında bir uç nokta ekle. API ayaktaysa ve veri dosyası okunabiliyorsa şunu döndürsün:
{"durum":"iyi","calismaSuresi":4,"surum":"yerel","takimSayisi":6}calismaSuresi: program başlayalı kaç tam saniye geçti.surum: Render'daysa yayındaki commit'in ilk 7 karakteri, kendi bilgisayarındaysa"yerel".takimSayisi: veri dosyasındaki takım sayısı. Dosya okunamazsa uç500dönmeli; sağlık ucu tam da bunu fark etmek için var.
Sınır denemesi: sunucuyu yeniden başlatıp hemen iste, calismaSuresi 0 ya da 1 olmalı; birkaç saniye bekleyip yeniden iste, artmalı.
İpucu 1
process.uptime(), programın kaç saniyedir çalıştığını küsuratlı olarak verir (5.0371 gibi). Küsuratı Math.floor atar.
İpucu 2
Render'ın yayındaki commit'i verdiği değişken RENDER_GIT_COMMIT; kendi bilgisayarında yoktur. process.env.RENDER_GIT_COMMIT?.slice(0, 7) ?? "yerel" iki durumu da karşılar.
İpucu 3
Route'u 404 ara katmanının üstüne yaz. Altına yazarsan /saglik'e gelen her istek 404 alır, çünkü 404 ara katmanı yanıtı önce gönderir ve route'una sıra hiç gelmez.
Çözümü göster
app.get("/saglik", async (req, res) => { const veri = await veriyiOku() res.json({ durum: "iyi", calismaSuresi: Math.floor(process.uptime()), surum: process.env.RENDER_GIT_COMMIT?.slice(0, 7) ?? "yerel", takimSayisi: veri.takimlar.length })})PS C:\kod\takim-api> curl.exe http://localhost:3000/saglik{"durum":"iyi","calismaSuresi":0,"surum":"yerel","takimSayisi":6}PS C:\kod\takim-api> curl.exe http://localhost:3000/saglik{"durum":"iyi","calismaSuresi":4,"surum":"yerel","takimSayisi":6}İki istek arasında 4 saniye bekledik; süre sende farklı çıkar. veriyiOku dosyayı okuyamazsa hata fırlatır ve hata yakalayıcı 500 döner: sağlık ucu dosyanın da sağlam olduğunu söylüyor.
Yayına alınca canlıdaki surum'un son commit'inle aynı olduğuna bak (git log --oneline ilk satırı); "son değişikliğim yayında mı?" sorusunun cevabı bu. Uyanmış bir serviste calismaSuresi küçük, takimSayisi yine 6'dır: canlıda eklediğin takımlar gitmiştir.
Render'a da bu ucu kullanmasını söyleyebilirsin: servisin Settings sayfasında Health Checks bölümüne /saglik yaz. Render belgesine göre bu adresi birkaç saniyede bir yoklar; yeni bir sürüm bu adrese 2xx ya da 3xx ile cevap vermeden trafiği ona geçirmez, çalışan servis 60 saniye boyunca cevap vermezse onu yeniden başlatır.
Takım arama
GET /api/takimlar?ara=... adın içinde geçen metne göre süzsün. Büyük-küçük harf fark etmesin ve Türkçe harfler doğru çalışsın. ara yoksa ya da boşsa bütün takımlar gelsin. Örnek istekler ve beklenen sonuçlar:
| İstek | Gelen takımlar |
|---|---|
?ara=kurt | Gece Kurtları, Kurt Sürüsü |
?ara=AVCI | Baron Avcıları |
?ara=KUŞ | Ateş Kuşları |
?ara= | altı takımın hepsi |
?ara=xyz | hiçbiri: [] |
İpucu 1
req.query.ara sorgudaki değeri metin olarak verir; sorguda yoksa undefined. Önce ?? "" ile boş metne çevir, sonra trim ile kenar boşluklarını at.
İpucu 2
Hem aranan metni hem takım adını aynı biçime getirip includes ile karşılaştır. Hangi küçültme fonksiyonu? "AVCI".toLowerCase() ile "AVCI".toLocaleLowerCase("tr")'yi yan yana dene.
İpucu 3
PowerShell'de & ve ? içeren adresleri tırnak içine al. Türkçe harfli aramaları (KUŞ) Invoke-RestMethod ile at; o, harfleri adrese doğru kodlar.
Çözümü göster
Önce iki küçültme fonksiyonunun farkı:
const ad = "Baron Avcıları"console.log("AVCI".toLowerCase())console.log("AVCI".toLocaleLowerCase("tr"))console.log(ad.toLocaleLowerCase("tr").includes("AVCI".toLowerCase()))console.log(ad.toLocaleLowerCase("tr").includes("AVCI".toLocaleLowerCase("tr")))avciavcıfalsetrueDüz toLowerCase büyük I'yı noktalı i yapıyor ve "avcı" ile eşleşmiyor (Ders 9'daki İ/ı tuzağı). Takım adları Türkçe olduğu için "tr" şart. Liste route'u:
app.get("/api/takimlar", async (req, res) => { const veri = await veriyiOku() const ara = (req.query.ara ?? "").trim().toLocaleLowerCase("tr") const bulunanlar = veri.takimlar.filter((t) => t.ad.toLocaleLowerCase("tr").includes(ara)) res.json(bulunanlar)})Boş metin her metnin içinde "geçtiği" için ara boşken includes("") hep true döner ve bütün takımlar gelir; ayrı bir if gerekmiyor.
PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?ara=kurt") | Select-Object id, ad id ad-- -- 1 Gece Kurtları 6 Kurt Sürüsü PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?ara=AVCI") | Select-Object id, ad id ad-- -- 3 Baron Avcıları PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?ara=KUŞ") | Select-Object id, ad id ad-- -- 5 Ateş Kuşları PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?ara=").Count6PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?ara=xyz"[]Sunucunun logunda Türkçe harfli istek GET /api/takimlar?ara=KU%C5%9E 200 1 ms diye görünür: adreslerde Türkçe harfler % ile başlayan kodlara çevrilerek gider. Express onları senin için geri çevirir.
Sayfalama
Takım sayısı binlere çıkınca hepsini tek yanıtta göndermek hem yavaş hem gereksiz olur. GET /api/takimlar listeyi sayfa sayfa versin: ?sayfa=2&boyut=4 "4'erli sayfaların ikincisi" demek. Kurallar:
sayfavarsayılan 1,boyutvarsayılan 10.sayfa1 ya da daha büyük bir tam sayı,boyut1 ile 50 arasında bir tam sayı olmalı; değilse400ve açıklama.- Yanıt yine bir dizi olsun, böylece API'ni kullanan eski programlar bozulmaz. Toplam takım sayısını
X-Toplam-Sayiadlı bir yanıt başlığında gönder. - Aramayla birlikte çalışsın: önce süz, sonra sayfala; başlıktaki sayı aramaya uyan takımların sayısı olsun.
Sınırlar: altı takımla ?sayfa=2&boyut=4 iki takım, ?sayfa=3&boyut=4 boş dizi vermeli. boyut=50 geçerli, boyut=51 değil. sayfa=0, sayfa=1.5 ve boyut=abc 400 almalı.
İpucu 1
Denetimi dogrula.js'e, sayfaBilgisi(sorgu) adlı bir fonksiyona koy: ya { sayfa, boyut } ya da { hata } döndürsün. Böylece onu sunucuyu açmadan deneyebilirsin. Number("2.5") 2.5 verir; tam sayı olup olmadığını Number.isInteger söyler (Ders 17).
İpucu 2
4'erli sayfalarda ikinci sayfa 5. takımdan başlar, yani dizide 4. sıradan. Genel kural: başlangıç sırası (sayfa - 1) * boyut, bitiş başlangıç + boyut. Ders 9'da metinlerde gördüğün slice dizilerde de aynı çalışır: başlangıcı alır, bitiş sırasını almaz.
İpucu 3
Express'te bir yanıt başlığı res.set("Ad", deger) ile eklenir. X- ile başlayan adlar, standart olmayan, uygulamaya özel başlıklar için eskiden beri kullanılan bir gelenek.
Çözümü göster
dogrula.js'in sonuna:
export function sayfaBilgisi(sorgu) { const sayfa = Number(sorgu.sayfa ?? 1) const boyut = Number(sorgu.boyut ?? 10) if (!Number.isInteger(sayfa) || sayfa < 1) { return { hata: "sayfa 1 ya da daha büyük bir tam sayı olmalı" } } if (!Number.isInteger(boyut) || boyut < 1 || boyut > 50) { return { hata: "boyut 1 ile 50 arasında bir tam sayı olmalı" } } return { sayfa, boyut }}Sunucuya bağlamadan önce sınırları dene. Fonksiyonu bir deneme dosyasına kopyalayıp çağırdık:
function sayfaBilgisi(sorgu) { const sayfa = Number(sorgu.sayfa ?? 1) const boyut = Number(sorgu.boyut ?? 10) if (!Number.isInteger(sayfa) || sayfa < 1) { return { hata: "sayfa 1 ya da daha büyük bir tam sayı olmalı" } } if (!Number.isInteger(boyut) || boyut < 1 || boyut > 50) { return { hata: "boyut 1 ile 50 arasında bir tam sayı olmalı" } } return { sayfa, boyut }} console.log(sayfaBilgisi({}))console.log(sayfaBilgisi({ sayfa: "2", boyut: "4" }))console.log(sayfaBilgisi({ boyut: "50" }))console.log(sayfaBilgisi({ boyut: "51" }))console.log(sayfaBilgisi({ sayfa: "0" }))console.log(sayfaBilgisi({ sayfa: "1.5" }))console.log(sayfaBilgisi({ sayfa: "" }))console.log(sayfaBilgisi({ sayfa: ["1", "2"] })){ sayfa: 1, boyut: 10 }{ sayfa: 2, boyut: 4 }{ sayfa: 1, boyut: 50 }{ hata: 'boyut 1 ile 50 arasında bir tam sayı olmalı' }{ hata: 'sayfa 1 ya da daha büyük bir tam sayı olmalı' }{ hata: 'sayfa 1 ya da daha büyük bir tam sayı olmalı' }{ hata: 'sayfa 1 ya da daha büyük bir tam sayı olmalı' }{ hata: 'sayfa 1 ya da daha büyük bir tam sayı olmalı' }Number("") 0 verir, bir dizi ise NaN: ikisi de reddedildi. sunucu.js'in en üstündeki içe aktarma satırına sayfaBilgisi'ni ekle ve liste route'unu şöyle yap:
app.get("/api/takimlar", async (req, res) => { const { sayfa, boyut, hata } = sayfaBilgisi(req.query) if (hata) { return res.status(400).json({ hata }) } const veri = await veriyiOku() const ara = (req.query.ara ?? "").trim().toLocaleLowerCase("tr") const bulunanlar = veri.takimlar.filter((t) => t.ad.toLocaleLowerCase("tr").includes(ara)) const bas = (sayfa - 1) * boyut res.set("X-Toplam-Sayi", bulunanlar.length) res.json(bulunanlar.slice(bas, bas + boyut))})const { sayfa, boyut, hata } = ... Ders 12'deki parçalama: dönen objede hangi alanlar varsa onlar dolar, ötekiler undefined kalır.
PS C:\kod\takim-api> curl.exe -i "http://localhost:3000/api/takimlar?ara=kurt&boyut=1"HTTP/1.1 200 OKX-Powered-By: ExpressX-Toplam-Sayi: 2Content-Type: application/json; charset=utf-8Content-Length: 200ETag: W/"c8-MJCga5d6F5AHYGA/IOSzrz/SbcM"Date: Sun, 11 Oct 2026 18:21:01 GMTConnection: keep-aliveKeep-Alive: timeout=5 [{"id":1,"ad":"Gece Kurtları","olusturulma":"2026-09-01T18:30:00.000Z","oyuncular":[{"kullaniciAdi":"deniz","rol":"orta"},{"kullaniciAdi":"ece","rol":"destek"}],"sampiyonlar":["Ahri","Lux","Garen"]}]PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?sayfa=2&boyut=4") | Select-Object id, ad id ad-- -- 5 Ateş Kuşları 6 Kurt Sürüsü PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?sayfa=3&boyut=4"[]PS C:\kod\takim-api> (Invoke-RestMethod "http://localhost:3000/api/takimlar?boyut=50").Count6PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?boyut=51"{"hata":"boyut 1 ile 50 arasında bir tam sayı olmalı"}PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?sayfa=0"{"hata":"sayfa 1 ya da daha büyük bir tam sayı olmalı"}PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?sayfa=1.5"{"hata":"sayfa 1 ya da daha büyük bir tam sayı olmalı"}PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?boyut=abc"{"hata":"boyut 1 ile 50 arasında bir tam sayı olmalı"}"kurt" iki takımda geçiyor; sayfada biri, başlıkta ikisi. Dördüncü görevi yaptıysan -i çıktısında bir başlık daha görürsün.
İstek kimliği
Her isteğe 8 karakterlik bir istek kimliği ver. Kimlik:
- yanıtta
X-Istek-Kimligibaşlığıyla gitsin, - kayıt ara katmanının her satırının başında köşeli parantez içinde dursun:
[e45995f5] GET /api/takimlar/2 200 5 ms, 500yanıtlarının gövdesineistekKimligialanı olarak eklensin ve hata yakalayıcının logladığı hatanın başına da yazılsın.
Sınırlar: arka arkaya iki istek farklı kimlik almalı; 404 yanıtlarında da başlık olmalı. 500'ü denemek için veri.json'un içini geçici olarak boşalt, bir istek at, sonra git restore veri.json ile geri getir.
İpucu 1
node:crypto'daki randomUUID() (Ders 31'de bu modülü tanıdın) her çağrıda yeni, rastgele bir kimlik üretir: a05d02a4-b6dc-4f93-a8a3-add99a9b257c gibi. İlk 8 karakteri slice(0, 8) ile alırsın. 8 onaltılık karakter dört milyardan fazla ihtimal demek; logları birbirinden ayırmaya yeter.
İpucu 2
Kimliği üreten yeni bir ara katmanı en üste, kayıt ara katmanından da önceye koy. Ürettiğin kimliği req.kimlik = ... ile isteğin üstüne yaz; sonraki ara katmanlar ve hata yakalayıcı aynı req'i görür (Ders 31'deki korunan route ara katmanının bulduğu kullanıcıyı req'e yazması gibi).
Çözümü göster
Kimliğin nasıl göründüğüne bak:
import { randomUUID } from "node:crypto" const kimlik = randomUUID()console.log(kimlik)console.log(kimlik.slice(0, 8))a05d02a4-b6dc-4f93-a8a3-add99a9b257ca05d02a4sunucu.js'te üç değişiklik:
import { randomUUID } from "node:crypto" app.use((req, res, next) => { req.kimlik = randomUUID().slice(0, 8) res.set("X-Istek-Kimligi", req.kimlik) next()}) console.log(`[${req.kimlik}] ${req.method} ${req.originalUrl} ${res.statusCode} ${sure} ms`) console.error(`[${req.kimlik}]`, hata) res.status(500).json({ hata: "Sunucuda bir hata oldu", istekKimligi: req.kimlik })PS C:\kod\takim-api> curl.exe -i http://localhost:3000/api/takimlar/2HTTP/1.1 200 OKX-Powered-By: ExpressX-Istek-Kimligi: e45995f5Content-Type: application/json; charset=utf-8Content-Length: 197ETag: W/"c5-N0X+XetqwH8s/PM5iaBuehp8NSg"Date: Sun, 11 Oct 2026 22:45:07 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"id":2,"ad":"Mavi Ejderler","olusturulma":"2026-09-02T09:15:00.000Z","oyuncular":[{"kullaniciAdi":"mert","rol":"alt"},{"kullaniciAdi":"selin","rol":"orman"}],"sampiyonlar":["Jinx","Thresh","Lux"]}PS C:\kod\takim-api> curl.exe -i http://localhost:3000/api/takimlar/99HTTP/1.1 404 Not FoundX-Powered-By: ExpressX-Istek-Kimligi: 8c2e3fc6Content-Type: application/json; charset=utf-8Content-Length: 29ETag: W/"1d-dROtwCE5hGBhioJB963Igac7VbI"Date: Sun, 11 Oct 2026 22:45:07 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Takım bulunamadı"}veri.json'u boşaltıp bir istek daha attık:
PS C:\kod\takim-api> curl.exe http://localhost:3000/api/takimlar/1{"hata":"Sunucuda bir hata oldu","istekKimligi":"37172073"}PS C:\kod\takim-api> git restore veri.json[e45995f5] GET /api/takimlar/2 200 5 ms[8c2e3fc6] GET /api/takimlar/99 404 1 ms[37172073] SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>) at veriyiOku (file:///C:/kod/takim-api/sunucu.js:14:17) at async file:///C:/kod/takim-api/sunucu.js:59:18[37172073] GET /api/takimlar/1 500 3 msKimlikler rastgele; seninkiler farklı olacak. Kullanıcı sana 37172073 derse logda o kimliği aratır, hem hatayı hem isteğin satırını bulursun. Kimliği yalnız 500'de değil her yanıtta göndermek, sorunu olan her isteği izlenebilir yapar.
Canlıda hata avı
API'n yayında ve ilk dört görev tamam. Bir arkadaşın "arama bazen bozuluyor" diyor; sana hata yanıtını gönderiyor:
{"hata":"Sunucuda bir hata oldu","istekKimligi":"48364e89"}Render'ın Logs sayfasında 48364e89'u aratınca şu satırları buluyorsun. Bunları biz, ikinci ve üçüncü görevin çözümüyle yazılmış kodda kendi bilgisayarımızda ürettik; Render'da her satırın başında bir de saat olur:
[aeea0cf6] GET /api/takimlar?ara=kurt 200 1 ms[48364e89] TypeError: (req.query.ara ?? "").trim is not a function at file:///C:/kod/takim-api/sunucu.js:51:39[48364e89] GET /api/takimlar?ara=kurt&ara=baron 500 3 ms[62d85dec] GET /api/takimlar?sayfa=1&sayfa=2 400 0 msAltı adımı uygula: hatayı yerelde üret, sebebini bul, düzelt ve sınırları dene. Düzeltmeden sonra ?ara=kurt&ara=baron 400 ve anlaşılır bir mesaj almalı; ?ara=kurt eskisi gibi çalışmalı. Satır numarası sende farklı olabilir.
İpucu 1
İki satırı karşılaştır: aynı uç nokta ?ara=kurt ile çalışmış, ?ara=kurt&ara=baron ile düşmüş. Fark nerede? curl.exe "http://localhost:3000/api/takimlar?ara=kurt&ara=baron" ile hatayı kendin üret.
İpucu 2
Hata trim is not a function diyor: req.query.ara'nın metin olmadığı bir durum var. Express 5 sorgu kısmını Node'un node:querystring modülündeki parse ile çözer. parse("ara=kurt&ara=baron")'ın ne verdiğine bir deneme dosyasında bak.
İpucu 3
Çözüm, kullanmadan önce türü denetlemek: ara verilmişse ama metin değilse 400. Peki son satırdaki ?sayfa=1&sayfa=2 neden 500 değil de 400 aldı?
Çözümü göster
Önce sebebi gör:
import { parse } from "node:querystring" console.log(parse("ara=kurt"))console.log(parse("ara=kurt&ara=baron"))console.log(typeof parse("ara=kurt&ara=baron").ara)[Object: null prototype] { ara: 'kurt' }[Object: null prototype] { ara: [ 'kurt', 'baron' ] }objectAynı parametre iki kez yazılınca değeri bir dizi oluyor; dizilerin trim metodu yok. ([Object: null prototype], objenin hiçbir hazır metot taşımadığını söyleyen bir not; şimdilik geçebilirsin.) Kimse bunu bilerek göndermeyebilir: bir web sayfasındaki form ya da elle yazılmış bir adres kolayca aynı adı iki kez koyar. Liste route'unda veriyiOku'nun hemen üstüne bir denetim ekle:
if (req.query.ara !== undefined && typeof req.query.ara !== "string") { return res.status(400).json({ hata: "ara yalnız bir kez yazılmalı" }) }PS C:\kod\takim-api> curl.exe -i "http://localhost:3000/api/takimlar?ara=kurt&ara=baron"HTTP/1.1 400 Bad RequestX-Powered-By: ExpressX-Istek-Kimligi: ad30649cContent-Type: application/json; charset=utf-8Content-Length: 42ETag: W/"2a-rHTWcl26yjURpu157IYCeWfuzjk"Date: Sun, 11 Oct 2026 18:21:29 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"ara yalnız bir kez yazılmalı"}PS C:\kod\takim-api> curl.exe "http://localhost:3000/api/takimlar?ara=kurt&boyut=1"[{"id":1,"ad":"Gece Kurtları","olusturulma":"2026-09-01T18:30:00.000Z","oyuncular":[{"kullaniciAdi":"deniz","rol":"orta"},{"kullaniciAdi":"ece","rol":"destek"}],"sampiyonlar":["Ahri","Lux","Garen"]}]?sayfa=1&sayfa=2 zaten 400 alıyordu: Number(["1", "2"]) NaN verir, sayfaBilgisi de onu reddeder. Üçüncü görevde dizi durumunu denemiştin; yazdığın denetim bu hatayı tesadüfen önlemişti. Dersin özü: dışarıdan gelen her değerin türü senin beklediğin olmayabilir; kullanmadan önce denetle. Düzeltmeyi commit'le, gönder ve yayın bitince aynı isteği canlıda tekrarla: 400 gelmeli.
Kendini sına
Soru 1
API'nde let ziyaret = 0 diye bir sayaç var ve her istekte bir artıyor. Render'ın ücretsiz planında ve Vercel'de bu sayaç nasıl davranır?
Cevabı göster
Render'da program tek bir süreç olarak açık kaldığı için sayaç istekler boyunca artar; ama servis uyuyunca, yeniden başlatılınca ya da yeni sürüm yüklenince sıfırdan başlar. Vercel'de her kopyanın kendi sayacı vardır: aynı anda açık iki kopya iki ayrı sayı tutar, kapanan kopyanın sayacı da gider. İkisinde de kalıcı ve ortak bir sayı istiyorsan değeri programın dışında, bir veritabanında tutman gerekir.
Soru 2
package.json'da "start": "node --env-file=.env sunucu.js" yazıyor. Kendi bilgisayarında npm start çalışıyor, Render'da yayın başarısız oluyor. Neden?
Cevabı göster
.env dosyası .gitignore'da olduğu için GitHub'a hiç gitmedi; Render'ın klonunda yok. Node --env-file ile verilen dosyayı bulamayınca programı hiç başlatmaz (.env: not found). start betiği yalnız node sunucu.js olmalı; ayarları Render'ın Environment sayfası verir. --env-file yalnız kendi bilgisayarında kullandığın dev betiğinde durmalı.
Soru 3
Render'ın Environment sayfasında SALT_OKUNUR değişkenine evet yazıp Save only'yi seçtin. Hemen ardından canlı adrese bir DELETE isteği attın ve takım silindi. Neden? Logda neye bakarak anlarsın?
Cevabı göster
Save only değişkeni kaydeder ama yayın başlatmaz; değişken bir sonraki yayına kadar etkisizdir. Üstelik sunucu.js ayarları program başlarken bir kez okuyor: çalışan program SALT_OKUNUR'u hiç görmedi. Logda program yeniden başlamadığı için Salt okunur: GET dışındaki istekler 503 alacak satırı yoktur, DELETE satırı da 503 değil 204 yazar. Çözüm Save and deploy (ya da elle yeni bir yayın): program yeni değişkenle yeniden başlar.
Soru 4
api.example.com'u Render'daki servisine bağlamak istiyorsun. DNS paneline hangi kaydı eklersin? Kök ad example.com için neden aynı türü kullanamazsın?
Cevabı göster
api adına, değeri takim-api-XXXX.onrender.com olan bir CNAME kaydı. CNAME'in iyi yanı, Render IP adreslerini değiştirse bile kaydın doğru kalması. Kök ada ise DNS'in kuralı gereği CNAME konamaz: CNAME konan bir adda başka hiçbir kayıt duramaz, oysa kök adda alan adının çalışması için zorunlu kayıtlar (örneğin alan adının hangi DNS sunucularında durduğunu söyleyenler) vardır; e-posta kayıtları da çoğu zaman oradadır. Kök ad için ya DNS şirketinin desteklediği ANAME/ALIAS kaydı ya da Render'ın verdiği IP adresine (216.24.57.1) bir A kaydı kullanılır.
Soru 5
api CNAME kaydının değerini yanlış yazmıştın (takm-api-XXXX.onrender.com), fark edip düzelttin. Render'daki Verify artık başarılı, ama arkadaşın düzeltmeden önce de denemiş ve şimdi bir saate yakındır Could not resolve host hatası aldığını söylüyor. DNS panelinde kaydın TTL'si 3600 görünüyor. Ne oldu, ne yapmalısın?
Cevabı göster
Arkadaşının bilgisayarı (ya da kullandığı DNS sunucusu) yanlış kaydın cevabını daha önce almış ve TTL süresi boyunca, yani 3600 saniye (bir saat) hatırlıyor. Kayıt doğru; eski cevabın süresi dolunca kendiliğinden düzelir. Yapman gereken beklemek. İleride bir kaydı değiştirmeyi planlıyorsan TTL'yi önceden kısaltmak bu bekleyişi azaltır.
Soru 6
Logda şu satırları görüyorsun. Ne olmuş, kodunda bir hata var mı?
POST /api/takimlar 201 12 msTakım API'si çalışıyor: http://localhost:10000Veri dosyası: /opt/render/project/src/veri.jsonGET /api/takimlar/7 404 1 msCevabı göster
Takım eklenmiş (201), sonra program yeniden başlamış: ikinci ve üçüncü satırlar programın açılırken yazdıkları. Ücretsiz servis boş kalınca uyumuş ya da Render onu yeniden başlatmış olabilir. Geçici disk ilk hâline döndüğü için 7 numaralı takım yok ve 404 geliyor. Kodda hata yok; JSON dosyasıyla kalıcı veri tutulamıyor. Çözüm veriyi programın dışında, bir veritabanında tutmak.
Soru 7
Render'da PORT ortam değişkenine 3000 girmeli misin?
Cevabı göster
Hayır. Render PORT'u kendisi verir (varsayılanı 10000) ve programın o portu dinlemesini bekler. Kodun process.env.PORT ?? 3000 ile zaten ortamdaki değeri okuyor; 3000 yalnız kendi bilgisayarında, değişken yokken devreye girer. Elle başka bir değer girmek gereksiz bir risk.
Soru 8
Canlı API'ne attığın ilk istek 50 saniye sürdü, hemen ardından attıkların 100 milisaniyenin altında. Neden? İlk görevdeki sağlık ucu bunu nasıl doğrular?
Cevabı göster
Servis uyuyordu; ilk istek onu uyandırdı ve programın yeniden başlamasını bekledi (soğuk başlangıç). Sonraki istekler açık programa geldiği için hızlı. /saglik'in calismaSuresi alanı, uyandırdıktan hemen sonra küçük bir sayı gösterir: program az önce başlamıştır.
Kayıt noktası
Görevleri yaptıysan README'deki uç nokta tablosuna yeni parametreleri (?ara=&sayfa=&boyut=) ve /saglik'i ekle; X-Toplam-Sayi ve X-Istek-Kimligi başlıklarını da bir cümleyle anlat. Sonra kaydet ve gönder:
Bir sonraki derste Blok 5 başlıyor: JSON dosyasının neden yetmediğini kendi gözünle görecek, bulutta ücretsiz bir PostgreSQL veritabanı açacak ve ilk tablonu kuracaksın.