Blok 4 · Ders 27
Node.js ile ilk sunucu
node:http ile kendi sunucunu yazıyor, adreslere göre farklı yanıt veriyor, JSON döndürüyor ve tarayıcıdan bağlanıyorsun.
Bu dersin sonunda
- Sunucunun ne olduğunu, portu ve
localhost'u apartman ve kapı numarası benzetmesiyle anlatabilirsin node:httpile bir sunucu yazıp ona tarayıcıdan,curl.exeile veInvoke-RestMethodile istek atabilirsinreq.methodvereq.url'e bakarak her adrese ayrı yanıt verebilir, bilinmeyen adreslere 404 döndürebilirsin- Doğru durum kodu ve
Content-Typebaşlığıyla JSON yanıt gönderebilirsin node --watchile kaydettiğin anda kendini yeniden başlatan bir çalışma düzeni kurabilirsin- POST gövdesini parça parça okuyup bozuk JSON'a çökmeden 400 ile cevap verebilirsin
Bu sayfada
- Sunucu: hiç bitmeyen program
- Port ve localhost: bina ve kapı numarası
- İlk sunucun
- Sunucuyla konuşmak
- Durdurmak, yeniden başlatmak ve node --watch
- İsteği tanımak: req.method ve req.url
- Yönlendirme: her adrese kendi cevabı
- Yanıtın etiketi: Content-Type
- JSON yanıt ve durum kodu
- Yönlendirme tablosu
- POST: gelen gövdeyi okumak
- Görevler
- Kendini sına
- Kayıt noktası
Şimdiye kadar hep masanın bir tarafındaydın: tarayıcıdan ya da terminalden başkasının sunucusuna istek attın, gelen JSON'u okudun. Bu derste masanın öbür tarafına geçiyorsun ve istekleri karşılayan programı kendin yazıyorsun. Hiç paket kurmayacaksın; Node'un içinde gelen node:http modülü yetiyor. Dersin sonunda şampiyon listesi veren, yeni şampiyon kabul eden ve bozuk istekleri çökmeden geri çeviren küçük bir JSON API'n olacak.
Sunucu: hiç bitmeyen program
Ders 1'den beri yazdığın programların ortak bir yanı var: son satıra gelince biterler. node ders5.js yazarsın, program çalışır, ekrana yazar ve terminal sana geri döner. Bir web sitesi böyle çalışamaz. Data Dragon'un programı senin isteğini yanıtlayıp kapansaydı, ondan sonra gelen milyonlarca oyuncu ne yapacaktı?
Sunucu (server), açıldıktan sonra kendiliğinden bitmeyen ve istek bekleyen programdır. İstek gelince ona bakar, bir yanıt hazırlayıp gönderir ve sıradakini beklemeye devam eder. Ders 18'de istemci ve sunucu ayrımını görmüştün; şimdi sunucu tarafını sen yazıyorsun.
Port ve localhost: bina ve kapı numarası
Bir bilgisayarda aynı anda birden çok sunucu çalışabilir: Ders 18'deki Live Server, bu derste yazacağın Node sunucusu, ileride bir veritabanı. Bilgisayara gelen bir istek bunlardan hangisine gideceğini nereden bilecek? Cevabı Ders 18'de gördün: port numarası. localhost (sayıyla 127.0.0.1) da "bu bilgisayar" demekti. Bu derste o kapılardan birinin arkasına kendi programını koyuyorsun.
localhost:3000 yazarak aynı apartmanın 3000 numaralı kapısını çalıyor. 8080 numaralı kapının arkasında kimse yok.Tarayıcıya http://localhost:3000/sampiyonlar yazdığında adresin her parçası ayrı bir iş görür:
| Parça | Örnek | Anlamı |
|---|---|---|
| protokol | http:// | hangi dille konuşulacak: HTTP |
| makine | localhost | hangi bina: kendi bilgisayarın |
| port | :3000 | hangi kapı: 3000 numaralı |
| yol | /sampiyonlar | kapıdan girince ne istediğin |
Port 0 ile 65535 arasında bir sayıdır. Üç kuralı bil:
- Bir kapıyı aynı anda yalnız bir program dinleyebilir. İkinci bir program aynı kapıyı açmaya kalkarsa hata alır; bu hatayı birazdan bilerek yapacaksın.
- Adreste port yazmazsan tarayıcı varsayılanı kullanır:
httpiçin 80,httpsiçin 443.https://ddragon.leagueoflegends.comaslında 443 numaralı kapıya gider. - Geliştirirken alışılmış numaralar var: Node sunucuları için çoğunlukla 3000, Live Server için 5500. Bunlar kural değil gelenek; boş olan herhangi bir numarayı seçebilirsin.
İlk sunucun
Sunucu için yeni bir proje klasörü aç. Terminalde cd C:\kod ile kod klasörüne geç, mkdir ilk-sunucu ile yeni klasörü oluştur ve cd ilk-sunucu ile içine gir. Sonra Ders 14'teki gibi package.json oluştur:
PS C:\kod\ilk-sunucu> npm init -yWrote to C:\kod\ilk-sunucu\package.json: { "name": "ilk-sunucu", "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ç (File → Open Folder). package.json'daki son satırı "type": "module" yap; böylece import yazabilirsin.
Şimdi sunucu.js adında bir dosya aç ve şunu yaz:
import http from "node:http" const port = 3000 const sunucu = http.createServer((req, res) => { res.end("Merhaba, ben senin ilk sunucunum")}) sunucu.listen(port, () => { console.log(`Sunucu çalışıyor: http://localhost:${port}`)})PS C:\kod\ilk-sunucu> node sunucu.jsSunucu çalışıyor: http://localhost:3000Program bitmedi: terminalde yeni bir PS C:\kod\ilk-sunucu> satırı çıkmadı. Sunucu 3000 numaralı kapıda bekliyor.
Satır satır:
import http from "node:http": Node'un içinde gelenhttpmodülünü al. Ders 17'dekinode:fs/promisesgibi;node:öneki "npm'den değil, Node'un kendisinden" demek, bir şey kurman gerekmez.http.createServer(fonksiyon): sunucuyu kurar. Verdiğin fonksiyon her istek geldiğinde bir kez çağrılır; Ders 10'daki callback'in ta kendisi.req(request, istek) gelen isteğin bilgilerini taşır;res(response, yanıt) geri göndereceğin yanıttır. Adları sen seçiyorsun, ama bütün Node dünyası bu kısaltmaları kullanır.res.end("..."): yanıtı bu metinle gönder ve bitir.sunucu.listen(port, fonksiyon): kapıyı aç, dinlemeye başla. Kapı açılınca ikinci fonksiyon bir kez çalışır ve adresi yazar.
Sunucuyla konuşmak
Önce tarayıcıyı dene: adres çubuğuna localhost:3000 yaz. Sayfada Merhaba, ben senin ilk sunucunum yazısı çıkar. İlk sunucun çalışıyor.
Terminalden de istek atabilirsin, ama sunucunun terminali meşgul: komut yazacak yer yok. İkinci bir terminal aç: VS Code'da terminal panelinin sağ üstündeki + düğmesi ya da menüden Terminal → New Terminal. İki terminal arasında panelin sağındaki listeden geçersin (arayüz sürümden sürüme biraz değişebilir). Yeni terminalde Ders 26'daki araçları kullan:
PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000Merhaba, ben senin ilk sunucunumPS C:\kod\ilk-sunucu> curl.exe -i http://localhost:3000HTTP/1.1 200 OKDate: Sun, 11 Oct 2026 13:14:28 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-Length: 32 Merhaba, ben senin ilk sunucunumPS C:\kod\ilk-sunucu> Invoke-RestMethod http://localhost:3000Merhaba, ben senin ilk sunucunum-i ile yanıtın başı da göründü. Durum kodu yazmadın ama 200 OK geldi: Node varsayılan olarak 200 gönderir. Content-Length: 32 gövdenin kaç bayt olduğunu söylüyor; onu da Node hesapladı. Bir başlığın eksik olduğuna dikkat et: Content-Type yok. Sunucu, gönderdiği şeyin ne olduğunu söylemiyor. Bunun bedelini birazdan göreceksin.
curl: (7) Failed to connect to localhost:3000Ne zaman çıkar? Sunucu çalışmıyorken istek atınca: başlatmayı unuttun, Ctrl+C ile durdurdun, sunucu bir hatayla çöktü ya da port numarasını yanlış yazdın.
PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000curl: (7) Failed to connect to localhost:3000 after 2272 ms: Could not connect to serverPS C:\kod\ilk-sunucu> Invoke-RestMethod http://localhost:3000Invoke-RestMethod : Uzak sunucuya bağlanılamıyor...İngilizce Windows'ta Invoke-RestMethod aynı durumda Unable to connect to the remote server yazar. Tarayıcı ise Ders 18'de gördüğün "Bu siteye ulaşılamıyor" sayfasını açar; bu kez altında "localhost bağlanmayı reddetti" (İngilizcesi "localhost refused to connect") ve ERR_CONNECTION_REFUSED yazar: apartman doğru, ama kapının arkasında kimse yok.
Nasıl düzelir? Sunucunun terminaline bak. Sunucu çalışıyor: ... satırı duruyor ve altında komut istemi yoksa sunucu ayakta; o zaman adresteki port ile koddaki port aynı mı, denetle. Komut istemi geri gelmişse sunucu durmuş: node sunucu.js ile yeniden başlat. Çöktüyse üstündeki hata mesajını oku.
Durdurmak, yeniden başlatmak ve node --watch
Sunucuyu durdurmak için onun terminaline tıkla ve Ctrl+C'ye bas; Ders 5'te sonsuz döngüyü de böyle durdurmuştun. Komut istemi geri gelir, kapı kapanır.
Şimdi bir deneme yap: sunucu çalışırken res.end içindeki metni değiştir, kaydet ve tarayıcıyı yenile. Eski metin gelmeye devam eder. Çünkü çalışan program dosyanın başlattığın andaki hâlidir; Node dosyayı başta bir kez okur. Yeni metni görmek için Ctrl+C ile durdurup node sunucu.js ile yeniden başlatman gerekir.
Her değişiklikte durdurup başlatmak yorucu. Bu işi Node senin yerine yapabilir:
PS C:\kod\ilk-sunucu> node --watch sunucu.jsSunucu çalışıyor: http://localhost:3000Metni değiştirip kaydet. Terminal temizlenir ve şu üç satır çıkar:
Change detected in 'C:\\kod\\ilk-sunucu\\sunucu.js'Restarting 'sunucu.js'Sunucu çalışıyor: http://localhost:3000Change detected "değişiklik algılandı", Restarting "yeniden başlatılıyor" demek. Yoldaki çift ters eğik çizgiler Node'un yazım biçimi; dert etme. Uzun komutu her seferinde yazmamak için Ders 14'teki npm betiklerini kullan. package.json'daki scripts kısmını şöyle değiştir:
"scripts": { "start": "node sunucu.js", "dev": "node --watch sunucu.js" },PS C:\kod\ilk-sunucu> npm run dev > ilk-sunucu@1.0.0 dev> node --watch sunucu.js Sunucu çalışıyor: http://localhost:3000Dersin geri kalanında sunucun hep npm run dev ile çalışsın: kaydet, sunucu kendini yenilesin, sen yalnız isteği tekrarla.
Error: listen EADDRINUSE: address already in use :::3000Ne zaman çıkar? 3000 numaralı kapıyı başka bir program dinlerken sunucuyu bir kez daha başlatınca. En sık sebep: sunucu başka bir terminalde hâlâ çalışıyor. VS Code'da terminal panelini X ile kapatmak sunucuyu durdurmaz, paneli yalnızca gizler. Hatayı kendin gör: sunucun npm run dev ile açıkken ikinci terminalde bir kez daha node sunucu.js yaz.
PS C:\kod\ilk-sunucu> node sunucu.jsnode:events:505 throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:2324:16) at listenInCluster (node:net:2433:12) at Server.listen (node:net:2558:7) at file:///C:/kod/ilk-sunucu/sunucu.js:9:8 ...Emitted 'error' event on Server instance at: at emitErrorNT (node:net:2412:8) at process.processTicksAndRejections (node:internal/process/task_queues:90:21) { code: 'EADDRINUSE', errno: -4091, syscall: 'listen', address: '::', port: 3000} Node.js v24.20.0EADDRINUSE "adres zaten kullanımda" (error: address in use) demek. :::3000 içindeki :: "bu bilgisayarın bütün adresleri" anlamına gelen yazım, sondaki 3000 kapı numarası. Hatayı tetikleyen yer sunucu.js:9, yani sunucu.listen(...) satırı. npm run dev ile çalıştırdıysan sonuna Failed running 'sunucu.js' satırı da eklenir.
Nasıl düzelir? Önce VS Code'daki terminal listesine bak; sunucuyu çalıştıran eski terminali bul ve Ctrl+C ile durdur. Terminal listesinde bir terminalin üstüne gelince çıkan çöp kutusu simgesi de o terminali içindeki sunucuyla birlikte kapatır (komutun adı Terminal: Kill the Active Terminal Instance). Bulamazsan kapıyı kimin tuttuğunu PowerShell'e sor:
PS C:\kod\ilk-sunucu> Get-NetTCPConnection -LocalPort 3000 -State Listen | Select-Object LocalPort, OwningProcess LocalPort OwningProcess--------- ------------- 3000 29452 PS C:\kod\ilk-sunucu> Get-Process -Id 29452 Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName------- ------ ----- ----- ------ -- -- ----------- 207 44 25196 62500 0,08 29452 1 node PS C:\kod\ilk-sunucu> Stop-Process -Id 29452OwningProcess, kapıyı tutan programın kimlik numarası (PID); sende başka bir sayı çıkar. Get-Process onun gerçekten node olduğunu gösterdi, Stop-Process -Id yalnız o programı kapattı. Adı node değil de başka bir şey çıksaydı kapıyı başka bir program tutuyor demekti; ne olduğunu bilmeden onu kapatma.
Bütün Node programlarını birden kapatan komutlardan (taskkill /IM node.exe gibi) da uzak dur: bilgisayarında başka işler için çalışan Node programları olabilir. Kapıyı kimin tuttuğunu bulamazsan en kısa çözüm koddaki portu değiştirmek: const port = 3001.
Bir tuzak daha: kapıyı tutan sunucu npm run dev ile başlatıldıysa Stop-Process yalnız asıl sunucuyu kapatır. Onu izleyen --watch ayakta kalır, Failed running 'sunucu.js' yazıp bekler ve dosyayı bir sonraki kaydedişinde sunucuyu yeniden başlatıp kapıyı yine tutar. Bu yüzden en temiz yol, eski terminali bulup Ctrl+C'ye basmak ya da çöp kutusuyla kapatmak.
İsteği tanımak: req.method ve req.url
Sunucu şu an her isteğe aynı cevabı veriyor. Önce gelen isteklere bir bakalım. Fonksiyonun ilk satırına bir console.log ekle ve kaydet:
const sunucu = http.createServer((req, res) => { console.log(req.method, req.url) res.end("Merhaba, ben senin ilk sunucunum")})Tarayıcıdan localhost:3000'i aç, sonra ikinci terminalden iki istek daha at:
PS C:\kod\ilk-sunucu> curl.exe "http://localhost:3000/sampiyonlar?rol=buyucu"Merhaba, ben senin ilk sunucunumPS C:\kod\ilk-sunucu> curl.exe -X DELETE http://localhost:3000/sampiyonlar/1Merhaba, ben senin ilk sunucunumSunucunun terminalinde her istek için bir satır belirir:
Change detected in 'C:\\kod\\ilk-sunucu\\sunucu.js'Restarting 'sunucu.js'Sunucu çalışıyor: http://localhost:3000GET /GET /favicon.icoGET /sampiyonlar?rol=buyucuDELETE /sampiyonlar/1req.methodisteğin yöntemi:GET,POST,DELETE... Tarayıcının adres çubuğu her zamanGETgönderir.curl.exe'de yöntemi-Xile seçersin.req.urladresin makine ve porttan sonraki kısmı.?ile başlayan sorgu kısmı da onun içinde:/sampiyonlar?rol=buyucu./favicon.icoisteğini sen göndermedin. Tarayıcı, sekmede göstereceği küçük simgeyi (favicon) kendiliğinden ister. Bir kez girdin, iki istek geldi.
Asıl sorun ortada: DELETE /sampiyonlar/1 bile "Merhaba" aldı. Her adrese ayrı bir cevap vermek için isteğe bakıp karar vermek gerekiyor.
Yönlendirme: her adrese kendi cevabı
Karar vermeyi Ders 2'den beri biliyorsun: if/else. Sunucuda buna yönlendirme (routing) denir: isteğin yöntemine ve yoluna bakıp hangi kodun çalışacağını seçmek. Fonksiyonu şöyle değiştir:
const sunucu = http.createServer((req, res) => { console.log(req.method, req.url) if (req.method === "GET" && req.url === "/") { res.end("Ana sayfa. Liste: /sampiyonlar") } else if (req.method === "GET" && req.url === "/sampiyonlar") { res.end("Ahri, Garen, Jinx") } else { res.writeHead(404) res.end("Sayfa yok") }})PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/Ana sayfa. Liste: /sampiyonlarPS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/sampiyonlarAhri, Garen, JinxPS C:\kod\ilk-sunucu> curl.exe -i http://localhost:3000/esyalarHTTP/1.1 404 Not FoundDate: Sun, 11 Oct 2026 13:14:52 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunked Sayfa yokres.writeHead(404) yanıtın başını yazar: "durum kodu 404". Başlık her zaman gövdeden önce gider; bu yüzden writeHead önce, res.end sonra gelir. Transfer-Encoding: chunked "gövde parçalar hâlinde gönderiliyor" demek: başı writeHead ile önceden yazdığımız için Node gövdenin uzunluğunu başa koyamadı ve gövdeyi bu biçimde gönderdi. Sorun değil.
Şu üç isteği de dene; üçü de 404 alır:
curl.exe -i -X POST http://localhost:3000/sampiyonlar: yol doğru, yöntem farklı.curl.exe -i http://localhost:3000/sampiyonlar/: sondaki/yüzünden başka bir adres.curl.exe -i "http://localhost:3000/sampiyonlar?rol=buyucu":req.urlsorgu kısmını da içeriyor,===tutmuyor.
Bilgisayar için /sampiyonlar, /sampiyonlar/ ve /Sampiyonlar üç ayrı adres. Sorgu kısmını ayırma işini Ders 28'de Express senin yerine yapacak.
res.end unutulduNe zaman çıkar? Bir dalda yanıtı göndermeyi unutunca. Fonksiyonlardan değer döndürmeye alıştığın için eline ilk gelen return olabilir; ama Node, createServer'a verdiğin fonksiyonun döndürdüğü değere hiç bakmaz.
if (req.method === "GET" && req.url === "/") { res.end("Ana sayfa. Liste: /sampiyonlar") } else if (req.method === "GET" && req.url === "/sampiyonlar") { return "Ahri, Garen, Jinx" } else {PS C:\kod\ilk-sunucu> curl.exe --max-time 5 http://localhost:3000/sampiyonlarcurl: (28) Operation timed out after 5014 milliseconds with 0 bytes receivedPS C:\kod\ilk-sunucu> Invoke-RestMethod http://localhost:3000/sampiyonlar -TimeoutSec 5Invoke-RestMethod : The operation has timed out....--max-time 5 curl'e "en çok 5 saniye bekle" der; vermezsen curl sonsuza dek bekler, Ctrl+C ile kesersin. Invoke-RestMethod'un mesajını denediğimiz Türkçe ve İngilizce Windows'ta da İngilizce çıktı: "işlem zaman aşımına uğradı". Tarayıcıda sekmenin yükleniyor simgesi döner durur. Sunucunun terminalinde hata yok, yalnızca GET /sampiyonlar satırı var: sunucu isteği aldı ama cevabı hiç göndermedi.
Nasıl düzelir? Her dal mutlaka bir res.end(...) ile bitmeli: res.end("Ahri, Garen, Jinx"). Yanıtsız bırakılan istek, kasada sonsuza dek bekletilen müşteri gibidir.
Yanıtın etiketi: Content-Type
Ana sayfanın metnini Türkçe bir cümleyle değiştir ve kaydet:
if (req.method === "GET" && req.url === "/") { res.end("Sihirdar Vadisi'ne hoş geldin")curl.exe cümleyi doğru yazar, ama tarayıcıda şunu görürsün:

ş harfini iki tuhaf karaktere çevirdi.Sunucu yalnızca baytları gönderdi; onların hangi kodlamayla okunacağını söylemedi. ş harfi UTF-8'de iki bayttır. Tarayıcı tahmin yürütüp eski bir kodlamayla okuyunca o iki baytı iki ayrı harf sandı: Å ve Ÿ. Çare, yanıtın başına bir etiket koymak:
if (req.method === "GET" && req.url === "/") { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }) res.end("Sihirdar Vadisi'ne hoş geldin")res.writeHead(durum, basliklar) önce durum kodunu, sonra başlıkları bir obje olarak alır. Content-Type: text/plain; charset=utf-8 (Ders 26'daki içerik türü) "bu düz bir metin ve UTF-8 ile yazıldı" demek. Kaydet, tarayıcıyı yenile: hoş artık doğru görünüyor. Sık kullanacağın üç değer:
Content-Type | Ne gönderiyorsun |
|---|---|
text/plain; charset=utf-8 | düz metin |
text/html; charset=utf-8 | HTML sayfası |
application/json; charset=utf-8 | JSON verisi |
JSON yanıt ve durum kodu
Bir API'nin işi insana cümle değil, programa veri göndermektir. Ders 24'te Data Dragon'dan JSON almıştın; şimdi kendi JSON'unu gönder. Her yanıtta aynı iki satırı yazmamak için küçük bir yardımcı fonksiyon kur. Dosyanın tamamı şöyle olsun:
import http from "node:http" const port = 3000 const sampiyonlar = [ { id: 1, isim: "Ahri", rol: "Büyücü" }, { id: 2, isim: "Garen", rol: "Dövüşçü" }, { id: 3, isim: "Jinx", rol: "Nişancı" }] function jsonGonder(res, durum, veri) { res.writeHead(durum, { "Content-Type": "application/json; charset=utf-8" }) res.end(JSON.stringify(veri))} const sunucu = http.createServer((req, res) => { console.log(req.method, req.url) if (req.method === "GET" && req.url === "/") { jsonGonder(res, 200, { mesaj: "Sihirdar Vadisi'ne hoş geldin" }) } else if (req.method === "GET" && req.url === "/sampiyonlar") { jsonGonder(res, 200, sampiyonlar) } else { jsonGonder(res, 404, { hata: `${req.method} ${req.url} diye bir yol yok` }) }}) sunucu.listen(port, () => { console.log(`Sunucu çalışıyor: http://localhost:${port}`)})jsonGonder üç şeyi tek yerde yapıyor: durum kodunu ve Content-Type'ı yazar, veriyi Ders 12'deki JSON.stringify ile metne çevirip gönderir.
PS C:\kod\ilk-sunucu> curl.exe -i http://localhost:3000/sampiyonlarHTTP/1.1 200 OKContent-Type: application/json; charset=utf-8Date: Sun, 11 Oct 2026 13:17:18 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunked [{"id":1,"isim":"Ahri","rol":"Büyücü"},{"id":2,"isim":"Garen","rol":"Dövüşçü"},{"id":3,"isim":"Jinx","rol":"Nişancı"}]PS C:\kod\ilk-sunucu> Invoke-RestMethod http://localhost:3000/sampiyonlar id isim rol-- ---- --- 1 Ahri Büyücü 2 Garen Dövüşçü 3 Jinx Nişancı PS C:\kod\ilk-sunucu> curl.exe -i http://localhost:3000/yokHTTP/1.1 404 Not FoundContent-Type: application/json; charset=utf-8Date: Sun, 11 Oct 2026 13:17:18 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunked {"hata":"GET /yok diye bir yol yok"}Invoke-RestMethod gelen JSON'u kendisi PowerShell nesnelerine çevirip tablo yaptı. curl.exe ise gövdeyi olduğu gibi yazar. 404'te bile JSON döndürdük: bir program "neden olmadı?" sorusunun cevabını da okuyabilmeli.
Tarayıcıda localhost:3000/sampiyonlar adresini aç. Chrome, Content-Type'tan bunun JSON olduğunu anladı ve üstüne bir kutu koydu:

Okunaklı hale getir kutusunu işaretlersen JSON girintili gösterilir:

Tarayıcının geliştirici araçlarını (F12) açıp Console sekmesine bakarsan kırmızı bir satır görebilirsin: Failed to load resource: the server responded with a status of 404 (Not Found). Bu, sekme simgesi için gelen /favicon.ico isteğinin 404 almasıdır; sunucun doğru davrandı.
Büyücü, hoÅŸNe zaman çıkar? JSON'u Content-Type başlığı olmadan gönderince ya da başlığa charset=utf-8 yazmayınca.
} else if (req.method === "GET" && req.url === "/sampiyonlar") { res.end(JSON.stringify(sampiyonlar))
Invoke-RestMethod de aynı yanıtta Büyücü yerine Büyücü yazar. Denediğimizde iki istemci farklı davrandı:
| Başlık | Tarayıcı | Invoke-RestMethod (PowerShell 5.1) |
|---|---|---|
| hiç yok | bozuk | bozuk |
application/json | doğru | bozuk |
application/json; charset=utf-8 | doğru | doğru |
Nasıl düzelir? Yanıtı hep jsonGonder ile gönder; başlık orada, charset=utf-8 ile birlikte yazılı. Başlığı tek bir yere koymanın faydası da bu: unutma şansın kalmaz.
Yönlendirme tablosu
if/else zinciri her yeni yolla uzar ve okuması zorlaşır. Yolları bir objede toplayabilirsin: anahtar "YÖNTEM yol" biçiminde bir metin, değer o isteği karşılayacak fonksiyon. jsonGonder'in altını şöyle değiştir:
function anaSayfa(req, res) { jsonGonder(res, 200, { mesaj: "Sihirdar Vadisi'ne hoş geldin" })} function sampiyonlariListele(req, res) { jsonGonder(res, 200, sampiyonlar)} const yollar = { "GET /": anaSayfa, "GET /sampiyonlar": sampiyonlariListele} const sunucu = http.createServer((req, res) => { console.log(req.method, req.url) const isleyici = yollar[`${req.method} ${req.url}`] if (isleyici) { isleyici(req, res) } else { jsonGonder(res, 404, { hata: `${req.method} ${req.url} diye bir yol yok` }) }})Hepsi bildiğin parçalar: anahtarı şablon metinle kuruyorsun ("GET /sampiyonlar"), objeden köşeli parantezle okuyorsun (Ders 4) ve fonksiyonları değer olarak saklıyorsun (Ders 10). Anahtarlarda boşluk ve / olduğu için onları tırnak içinde yazdın; Ders 26'daki "Content-Type" başlığında tire yüzünden yaptığın gibi. Olmayan anahtar undefined verir, if (isleyici) de onu yakalayıp 404'e gönderir. İstekleri yeniden at; cevaplar aynı. Fark şu: yeni bir yol artık bir fonksiyon ve tabloda tek satır.
POST) birazdan ekleyeceksin.POST: gelen gövdeyi okumak
Sunucu şimdiye kadar yalnız veri verdi. Yeni bir şampiyon eklemek için istemci veriyi isteğin gövdesinde gönderir (Ders 26'daki POST). Ama gövde, req.url gibi hazır duran bir alan değildir: ağdan parça parça gelir.
Parçaların neye benzediğini ağ olmadan görebilirsin. Proje klasöründe parca-deneme.js adında bir dosya aç; bir JSON'u ortasından ikiye bölüp iki parça yapalım. İkinci terminalde node parca-deneme.js ile çalıştır (sunucu bu dosyayı izlemediği için yeniden başlamaz):
const parcalar = [Buffer.from('{"isim": "Ji'), Buffer.from('nx", "rol": "Nişancı"}')]console.log(parcalar[0])console.log(parcalar[0].toString()) const metin = Buffer.concat(parcalar).toString()console.log(metin)console.log(JSON.parse(metin).isim)<Buffer 7b 22 69 73 69 6d 22 3a 20 22 4a 69>{"isim": "Ji{"isim": "Jinx", "rol": "Nişancı"}JinxAğdan gelen her parça bir Buffer'dır: ham baytlar. console.log onları iki haneli sayılarla gösterir; 7b süslü parantezin, 22 çift tırnağın baytı. Bu sayılar Ders 20'deki renk kodları gibi onaltılık yazılır, o yüzden içlerinde a–f harfleri de olur; okuyabilmen gerekmiyor.
.toString() baytları metne çevirir, Buffer.concat parçaları uç uca ekler. JSON metnini Ders 12'deki gibi tek tırnakla yazdık, çünkü içinde çift tırnak var.
Peki bütün parçaları beklemeden, yalnız ilk parçayı okumaya kalksaydık? Dosyanın içeriğini şununla değiştir:
const ilkParca = Buffer.from('{"isim": "Ji')const veri = JSON.parse(ilkParca.toString())console.log(veri.isim)<anonymous_script>:1{"isim": "Ji SyntaxError: Unterminated string in JSON at position 12 (line 1 column 13) at JSON.parse (<anonymous>) at file:///C:/kod/ilk-sunucu/parca-deneme.js:2:19 ... Node.js v24.20.0Unterminated string: "bitmemiş metin". Yarım JSON okunamaz. En üstteki <anonymous_script>:1 senin dosyan değil, JSON.parse'a verdiğin metin; senin dosyandaki yeri at satırı gösteriyor: parca-deneme.js:2.
Gövdeyi okuyan fonksiyon
sunucu.js'e dön. jsonGonder'in hemen altına şu fonksiyonu ekle:
async function govdeyiOku(req) { const parcalar = [] for await (const parca of req) { parcalar.push(parca) } return Buffer.concat(parcalar).toString()}req bir akış (stream): verisi zamanla, parça parça gelen bir kaynak. for await (const parca of req) Ders 5'teki for...of'a benziyor; farkı, her turda bir sonraki parçanın ağdan gelmesini await ile beklemesi. İstemci gövdenin bittiğini bildirince döngü de biter. İçinde await olduğu için fonksiyon async olmalı (Ders 15); sonucu da await govdeyiOku(req) ile alırsın.
SyntaxError: Unexpected reserved wordNe zaman çıkar? for await ya da await yazdığın fonksiyonun başına async koymayı unutunca.
function govdeyiOku(req) { const parcalar = [] for await (const parca of req) {Change detected in 'C:\\kod\\ilk-sunucu\\sunucu.js'Restarting 'sunucu.js'file:///C:/kod/ilk-sunucu/sunucu.js:18 for await (const parca of req) { ^^^^^ SyntaxError: Unexpected reserved word ... Node.js v24.20.0Failed running 'sunucu.js'. Waiting for file changes before restarting...async olmayan bir fonksiyonun içinde await kullanılamaz. JavaScript onu orada "ayrılmış sözcük" (reserved word) sayar ve, Ders 1'deki öteki SyntaxError'lar gibi, dosyanın hiçbir satırını çalıştırmaz: sunucu hiç açılmaz.
Nasıl düzelir? Fonksiyonun başına async ekle: async function govdeyiOku(req).
Şampiyon eklemek
Şimdi gövdeyi okuyup listeye ekleyen fonksiyonu yaz, tabloya bir satır ekle ve sunucunun fonksiyonunu async yap:
async function sampiyonEkle(req, res) { const metin = await govdeyiOku(req) const veri = JSON.parse(metin) const yeni = { id: sampiyonlar.length + 1, isim: veri.isim, rol: veri.rol ?? "Bilinmiyor" } sampiyonlar.push(yeni) jsonGonder(res, 201, yeni)} const yollar = { "GET /": anaSayfa, "GET /sampiyonlar": sampiyonlariListele, "POST /sampiyonlar": sampiyonEkle} const sunucu = http.createServer(async (req, res) => { console.log(req.method, req.url) const isleyici = yollar[`${req.method} ${req.url}`] if (isleyici) { await isleyici(req, res) } else { jsonGonder(res, 404, { hata: `${req.method} ${req.url} diye bir yol yok` }) }})rol gönderilmezse ?? (Ders 12) yerine "Bilinmiyor" koyar. Yeni kayıt için 201 Created dönüyoruz. await isleyici(req, res), işleyici sampiyonEkle gibi bir async fonksiyonsa onun bitmesini bekler; anaSayfa gibi sıradan bir fonksiyonda da sorun çıkarmaz. İçinde await olduğu için sunucunun fonksiyonuna da async yazdık.
Dene; Invoke-RestMethod bir PowerShell komutu olduğu için tek tırnak içindeki JSON olduğu gibi gider:
PS C:\kod\ilk-sunucu> Invoke-RestMethod -Method Post -Uri http://localhost:3000/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Lux", "rol": "Büyücü"}' id isim rol-- ---- --- 4 Lux Büyücü -ContentType'taki charset=utf-8'i atlama. Denediğimizde, onsuz gönderilen Büyücü sunucuya B�y�c� olarak ulaştı: PowerShell 5.1 gövdeyi başka bir kodlamayla yolladı.
Parçaları kendi gözünle görmek istersen döngüye geçici bir satır ekle, sonra bir küçük, bir de 200.000 harflik büyük gövde gönder. PowerShell'de 'A' * 200000, 200.000 tane A'dan oluşan bir metin üretir:
for await (const parca of req) { console.log("Parça geldi:", parca.length, "bayt") parcalar.push(parca) }PS C:\kod\ilk-sunucu> Invoke-RestMethod -Method Post -Uri http://localhost:3000/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Lux", "rol": "Büyücü"}' id isim rol-- ---- --- 4 Lux Büyücü PS C:\kod\ilk-sunucu> (Invoke-RestMethod -Method Post -Uri http://localhost:3000/sampiyonlar -ContentType "application/json; charset=utf-8" -Body ('{"isim": "' + ('A' * 200000) + '"}')).id5Dosyayı kaydettiğin için sunucu yeniden başladı ve liste sıfırlandı; Lux yine 4 numara aldı. Büyük gövdeli şampiyonun bütün bilgisini yazdırmak terminali 200.000 harfle doldururdu, bu yüzden komutun sonundaki .id ile yalnız numarasını aldık.
POST /sampiyonlarParça geldi: 35 baytPOST /sampiyonlarParça geldi: 65536 baytParça geldi: 65536 baytParça geldi: 65536 baytParça geldi: 3404 baytLux'un küçük gövdesi tek parçada geldi; büyük gövde dört parçaya bölündü. Parça boyutları sende farklı olabilir. Denemeden sonra o console.log satırını sil.
SyntaxError: Unexpected token 'L', "Lux" is not valid JSONNe zaman çıkar? Gövdesi JSON olmayan bir POST geldiğinde, JSON.parse try içinde değilse. Tek bir bozuk istek bütün sunucuyu düşürür.
curl.exe --json "metin" metni gövde yapıp POST eder ve Content-Type: application/json başlığını kendisi ekler. Bilerek bozuk bir gövde gönder:
PS C:\kod\ilk-sunucu> curl.exe -i --json "Lux" http://localhost:3000/sampiyonlarcurl: (56) Recv failure: Connection was resetPOST /sampiyonlar<anonymous_script>:1Lux^ SyntaxError: Unexpected token 'L', "Lux" is not valid JSON at JSON.parse (<anonymous>) at sampiyonEkle (file:///C:/kod/ilk-sunucu/sunucu.js:34:23) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) at async Server.<anonymous> (file:///C:/kod/ilk-sunucu/sunucu.js:50:9) Node.js v24.20.0Failed running 'sunucu.js'. Waiting for file changes before restarting...Aynı isteği Invoke-RestMethod ile atsaydın Temel alınan bağlantı kapatıldı: Alma işleminde beklenmeyen hata oluştu. (İngilizce Windows'ta The underlying connection was closed: An unexpected error occurred on a receive.) görürdün. Yığın izini Ders 13'te öğrendiğin gibi oku: hata JSON.parse'ta çıktı, onu sampiyonEkle çağırdı (sunucu.js:34). Sunucu çöktü; sıradaki her istek Failed to connect alır.
Nasıl düzelir? Dışarıdan gelen veriye güvenme. JSON.parse'ı try içine al, hata olursa 400 gönder ve return ile fonksiyondan çık. Düzeltilmiş hâli hemen aşağıda.
Bozuk isteğe 400 demek
sampiyonEkle'yi şöyle değiştir:
async function sampiyonEkle(req, res) { const metin = await govdeyiOku(req) let veri try { veri = JSON.parse(metin) } catch (hata) { console.log("Bozuk JSON geldi:", metin) jsonGonder(res, 400, { hata: "Gövde geçerli bir JSON değil", ayrinti: hata.message }) return } if (!veri?.isim) { jsonGonder(res, 400, { hata: "isim alanı gerekli" }) return } const yeni = { id: sampiyonlar.length + 1, isim: veri.isim, rol: veri.rol ?? "Bilinmiyor" } sampiyonlar.push(yeni) jsonGonder(res, 201, yeni)}let veritry'ın dışında, çünkü bloğun içinde açılan kutu dışarıdan görünmez (Ders 10).catchbozuk metni sunucunun terminaline yazar ve istemciye sebebiyle birlikte400döner.hata.message,JSON.parse'ın kendi açıklaması.!veri?.isimhemisimgelmediğinde hem gövdenullolduğunda 400 verir;?.(Ders 12) olmasaydınullgövde yine sunucuyu çökertirdi. Alanları tek tek doğrulamayı Ders 28'de düzenli hâle getireceksin.
Kaydet ve sınır durumlarını dene:
PS C:\kod\ilk-sunucu> curl.exe -i --json "Lux" http://localhost:3000/sampiyonlarHTTP/1.1 400 Bad RequestContent-Type: application/json; charset=utf-8Date: Sun, 11 Oct 2026 13:21:43 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunked {"hata":"Gövde geçerli bir JSON değil","ayrinti":"Unexpected token 'L', \"Lux\" is not valid JSON"}PS C:\kod\ilk-sunucu> curl.exe -X POST http://localhost:3000/sampiyonlar{"hata":"Gövde geçerli bir JSON değil","ayrinti":"Unexpected end of JSON input"}PS C:\kod\ilk-sunucu> curl.exe --json "{}" http://localhost:3000/sampiyonlar{"hata":"isim alanı gerekli"}PS C:\kod\ilk-sunucu> curl.exe --json "null" http://localhost:3000/sampiyonlar{"hata":"isim alanı gerekli"}Boş gövde (-X POST ile hiçbir şey göndermeden) de 400 aldı: JSON.parse("") "beklenmedik JSON sonu" der. Sunucu dört bozuk isteğin hiçbirinde çökmedi.
Geçerli bir JSON'u curl.exe ile göndermenin en sağlam yolu onu bir dosyaya yazmak. VS Code'da lux.json dosyası aç, içine { "isim": "Lux", "rol": "Büyücü" } yaz ve kaydet:
PS C:\kod\ilk-sunucu> curl.exe --json "@lux.json" http://localhost:3000/sampiyonlar{"id":4,"isim":"Lux","rol":"Büyücü"}@lux.json curl'e "gövdeyi bu dosyadan oku" der. Tırnağı unutma: PowerShell'de sözcük başındaki @ özel bir işarettir. Kaydettiğinden beri başka şampiyon eklediysen id sende daha büyük çıkar.
{isim: Teemo} aldı: tırnaklar nerede?Ne zaman çıkar? Windows 11 ile gelen Windows PowerShell 5.1'de, tırnaklı JSON'u curl.exe'nin komut satırına doğrudan yazınca. PowerShell 5.1, curl.exe gibi dış programlara argüman verirken içteki çift tırnakları siler. Bu, Ders 26'daki tırnak tuzağı; bu kez sunucu senin, o yüzden neyin geldiğini kendi gözünle görebilirsin.
PS C:\kod\ilk-sunucu> curl.exe --json '{"isim": "Teemo"}' http://localhost:3000/sampiyonlar{"hata":"Gövde geçerli bir JSON değil","ayrinti":"Expected property name or '}' in JSON at position 1 (line 1 column 2)"}JSON'un doğru görünüyor, ama sunucunun terminali gerçekte neyin geldiğini söylüyor:
POST /sampiyonlarBozuk JSON geldi: {isim: Teemo}catch'e eklediğin console.log işte burada işe yaradı: anahtarın etrafında tırnak olmadığı için JSON geçersiz.
Nasıl düzelir? JSON'u bir dosyaya yaz ve "@dosya.json" ile gönder ya da Invoke-RestMethod -Body kullan; ikisi de tırnaklara dokunmaz. PowerShell 7.3 ve sonrasında bu sorun yok. Hangi sürümü kullandığını $PSVersionTable.PSVersion gösterir.
Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the clientNe zaman çıkar? Aynı isteğe ikinci kez yanıt yazmaya çalışınca. En sık sebep: hata yanıtını gönderdikten sonra return yazmayı unutmak. Fonksiyon çalışmaya devam eder ve bir yanıt daha göndermeye kalkar.
} catch (hata) { console.log("Bozuk JSON geldi:", metin) jsonGonder(res, 400, { hata: "Gövde geçerli bir JSON değil", ayrinti: hata.message }) } if (!veri?.isim) { jsonGonder(res, 400, { hata: "isim alanı gerekli" })POST /sampiyonlarBozuk JSON geldi: Luxnode:_http_server:411 throw new ERR_HTTP_HEADERS_SENT('write'); ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the client at ServerResponse.writeHead (node:_http_server:411:11) at jsonGonder (file:///C:/kod/ilk-sunucu/sunucu.js:12:9) at sampiyonEkle (file:///C:/kod/ilk-sunucu/sunucu.js:42:9) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) at async Server.<anonymous> (file:///C:/kod/ilk-sunucu/sunucu.js:60:9) { code: 'ERR_HTTP_HEADERS_SENT'} Node.js v24.20.0Failed running 'sunucu.js'. Waiting for file changes before restarting...Sinsi yanı şu: istemci ilk yanıtı, yani 400'ü sorunsuz aldı; her şey yolunda sandın. Ama veri boş kaldığı için sonraki if de çalıştı ve ikinci jsonGonder (sunucu.js:42) çoktan gitmiş bir yanıtın başını yeniden yazmaya kalktı. Sunucu çöktü.
Nasıl düzelir? Yanıt gönderen her dalın sonuna return koy. Kural: bir istek, bir yanıt.
Sunucunun son hâli tek parça hâlinde böyle. Kendi dosyanla karşılaştır; klasöründe bunun yanında package.json, lux.json ve parca-deneme.js duruyor:
import http from "node:http" const port = 3000 const sampiyonlar = [ { id: 1, isim: "Ahri", rol: "Büyücü" }, { id: 2, isim: "Garen", rol: "Dövüşçü" }, { id: 3, isim: "Jinx", rol: "Nişancı" }] function jsonGonder(res, durum, veri) { res.writeHead(durum, { "Content-Type": "application/json; charset=utf-8" }) res.end(JSON.stringify(veri))} async function govdeyiOku(req) { const parcalar = [] for await (const parca of req) { parcalar.push(parca) } return Buffer.concat(parcalar).toString()} function anaSayfa(req, res) { jsonGonder(res, 200, { mesaj: "Sihirdar Vadisi'ne hoş geldin" })} function sampiyonlariListele(req, res) { jsonGonder(res, 200, sampiyonlar)} async function sampiyonEkle(req, res) { const metin = await govdeyiOku(req) let veri try { veri = JSON.parse(metin) } catch (hata) { console.log("Bozuk JSON geldi:", metin) jsonGonder(res, 400, { hata: "Gövde geçerli bir JSON değil", ayrinti: hata.message }) return } if (!veri?.isim) { jsonGonder(res, 400, { hata: "isim alanı gerekli" }) return } const yeni = { id: sampiyonlar.length + 1, isim: veri.isim, rol: veri.rol ?? "Bilinmiyor" } sampiyonlar.push(yeni) jsonGonder(res, 201, yeni)} const yollar = { "GET /": anaSayfa, "GET /sampiyonlar": sampiyonlariListele, "POST /sampiyonlar": sampiyonEkle} const sunucu = http.createServer(async (req, res) => { console.log(req.method, req.url) const isleyici = yollar[`${req.method} ${req.url}`] if (isleyici) { await isleyici(req, res) } else { jsonGonder(res, 404, { hata: `${req.method} ${req.url} diye bir yol yok` }) }}) sunucu.listen(port, () => { console.log(`Sunucu çalışıyor: http://localhost:${port}`)})69 satırlık, paketsiz bir JSON API: üç yol, gövde okuma, 201, 400 ve 404.
Görevler
Görevleri aynı sunucu.js üzerinde yap; sunucu npm run dev ile açık kalsın. Her görevden sonra istekleri curl.exe -i ile atıp durum kodunu ve gövdeyi kendi gözünle denetle. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Eşya listesi
Sunucuya GET /esyalar yolunu ekle. Şu üç eşyayı JSON olarak, 200 ile döndürsün:
Uzun Kılıç, 350 altınBüyü Kitabı, 435 altınÇevik Botlar, 300 altınBeklenen:
PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/esyalar[{"id":1,"isim":"Uzun Kılıç","fiyat":350},{"id":2,"isim":"Büyü Kitabı","fiyat":435},{"id":3,"isim":"Çevik Botlar","fiyat":300}]Sonra şu üç isteğin neden 404 aldığını kendine açıkla: GET /esyalar/, GET /Esyalar, POST /esyalar.
İpucu 1
Üç şey eklemen gerekiyor: bir esyalar dizisi (her eşya { id, isim, fiyat } alanlı bir obje), bir listeleme fonksiyonu ve yollar tablosuna bir satır.
İpucu 2
Fonksiyon sampiyonlariListele'nin ikizi: jsonGonder(res, 200, esyalar). Tablodaki anahtar "GET /esyalar".
Çözümü göster
const esyalar = [ { id: 1, isim: "Uzun Kılıç", fiyat: 350 }, { id: 2, isim: "Büyü Kitabı", fiyat: 435 }, { id: 3, isim: "Çevik Botlar", fiyat: 300 }]function esyalariListele(req, res) { jsonGonder(res, 200, esyalar)}yollar tablosuna da "GET /esyalar": esyalariListele satırını ekle (bir önceki satırın sonuna virgül koymayı unutma).
PS C:\kod\ilk-sunucu> curl.exe -i http://localhost:3000/esyalarHTTP/1.1 200 OKContent-Type: application/json; charset=utf-8Date: Sun, 11 Oct 2026 13:32:46 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunked [{"id":1,"isim":"Uzun Kılıç","fiyat":350},{"id":2,"isim":"Büyü Kitabı","fiyat":435},{"id":3,"isim":"Çevik Botlar","fiyat":300}]PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/Esyalar{"hata":"GET /Esyalar diye bir yol yok"}Üç 404'ün sebebi: tablodaki anahtarlar harfi harfine eşleşmeli. GET /esyalar/ sonda fazladan / taşıyor, /Esyalar büyük harfle başlıyor, POST /esyalar ise başka bir yöntem; tabloda üçünün de anahtarı yok.
Ziyaret sayacı
GET /sayac yolunu ekle. Her istekte bir artan bir sayı döndürsün: ilk istekte {"ziyaret":1}, ikincide {"ziyaret":2}... Sonra sunucu.js'i değiştirmeden bir kez daha kaydet ve sayacı yeniden iste. Ne oldu, neden?
İpucu 1
Sayıyı fonksiyonun dışında, let ile tutman gerekiyor. Fonksiyonun içinde açılan kutu her istekte sıfırdan açılır.
İpucu 2
Fonksiyonun içinde önce sayıyı bir artır, sonra jsonGonder(res, 200, { ziyaret: ziyaret }) ile gönder. İlk istekte 0 değil 1 dönmeli; artırmayı göndermeden önce yap.
Çözümü göster
let ziyaret = 0 function sayac(req, res) { ziyaret = ziyaret + 1 jsonGonder(res, 200, { ziyaret: ziyaret })}Tabloya "GET /sayac": sayac satırını ekle.
PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/sayac{"ziyaret":1}PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/sayac{"ziyaret":2}PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/sayac{"ziyaret":3}# sunucu.js'i değiştirmeden kaydettikten sonra:PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/sayac{"ziyaret":1}Dosyayı kaydedince (VS Code'da Ctrl+S değişiklik olmasa da dosyayı yeniden yazar) --watch sunucuyu yeniden başlattı, ziyaret yeniden 0'dan başladı ve sıradaki istek {"ziyaret":1} döndü. Değişkenler programın belleğinde yaşar; program yeniden başlayınca bellek de sıfırlanır. Tarayıcıdan da dene: her yenilemede bir artar, çünkü /favicon.ico isteği başka bir yola gidiyor.
Hata avı: altın sunucusu
Aşağıdaki sunucunun üç hatası var. Proje klasöründe altin.js adıyla kaydet, yeni bir terminal aç (+ düğmesi) ve orada node --watch altin.js ile çalıştır. 3001 numaralı kapıyı kullandığı için ana sunucunla çakışmaz; istekleri yine her zamanki istek terminalinden at. Hataları gözle aramak yerine belirtilerden yola çık.
import http from "node:http" const port = 3001let altin = 50 function jsonGonder(res, durum, veri) { res.writeHead(durum, { "Content-Type": "application/json; charset=utf-8" }) res.end(JSON.stringify(veri))} const sunucu = http.createServer((req, res) => { console.log(req.method, req.url) if (req.method === "GET" && req.url === "/altin") { res.end(JSON.stringify({ altin: altin, not: "Faiz için en az 10 altın gerekir" })) } else if (req.method === "GET" && req.url === "/faiz") { const faiz = Math.min(Math.floor(altin / 10), 5) } else if (req.method === "POST" && req.url === "/harca") { if (altin < 10) { jsonGonder(res, 400, { hata: "Altın yetmiyor" }) } altin = altin - 10 jsonGonder(res, 200, { altin: altin }) } else { jsonGonder(res, 404, { hata: "Böyle bir yol yok" }) }}) sunucu.listen(port, () => { console.log(`Altın sunucusu çalışıyor: http://localhost:${port}`)})Belirtiler:
Invoke-RestMethod http://localhost:3001/altinnotuFaiz için en az 10 altın gerekirdiye yazıyor.curl.exe --max-time 5 http://localhost:3001/faizşunu veriyor:curl: (28) Operation timed out after 5012 milliseconds with 0 bytes received.curl.exe -X POST http://localhost:3001/harcailk beş seferde40,30,20,10,0döndürüyor. Altıncıda{"hata":"Altın yetmiyor"}geliyor, ama hemen ardından sunucuERR_HTTP_HEADERS_SENTile çöküyor.
Düzeltince /faiz başta {"faiz":5} dönmeli (TFT'de her 10 altına 1, en çok 5 faiz), altın bitince {"faiz":0} dönmeli ve sunucu kaç kez harcarsan harca ayakta kalmalı.
İpucu 1
Her belirti bu derste bir hata kartına karşılık geliyor: bozuk harfler, hiç bitmeyen istek ve çift yanıt.
İpucu 2
1: /altin dalı yanıtı jsonGonder olmadan, başlıksız gönderiyor. 2: /faiz dalı faizi hesaplıyor ama göndermiyor. 3: altin < 10 dalı 400'ü gönderdikten sonra fonksiyondan çıkmıyor.
Çözümü göster
if (req.method === "GET" && req.url === "/altin") { jsonGonder(res, 200, { altin: altin, not: "Faiz için en az 10 altın gerekir" }) } else if (req.method === "GET" && req.url === "/faiz") { const faiz = Math.min(Math.floor(altin / 10), 5) jsonGonder(res, 200, { faiz: faiz }) } else if (req.method === "POST" && req.url === "/harca") { if (altin < 10) { jsonGonder(res, 400, { hata: "Altın yetmiyor" }) return } altin = altin - 10 jsonGonder(res, 200, { altin: altin }) } else {PS C:\kod\ilk-sunucu> curl.exe http://localhost:3001/faiz{"faiz":5}PS C:\kod\ilk-sunucu> curl.exe -X POST http://localhost:3001/harca{"altin":40}# aynı harcamayı üç kez daha yap: 30, 20, 10PS C:\kod\ilk-sunucu> curl.exe -X POST http://localhost:3001/harca{"altin":0}PS C:\kod\ilk-sunucu> curl.exe http://localhost:3001/faiz{"faiz":0}PS C:\kod\ilk-sunucu> curl.exe -X POST http://localhost:3001/harca{"hata":"Altın yetmiyor"}Sınıra bak: altın tam 10 iken yapılan harcama kabul edildi ve 0 kaldı, çünkü koşul altin < 10. Sonraki her harcama 400 alır ve sunucu artık ayakta kalır.
Tek eşya getir
GET /esyalar/2 gibi bir istek, id'si 2 olan eşyayı döndürsün. Olmayan bir numaraya 404, sayı olmayan bir id'ye 400 dönsün:
PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/esyalar/2{"id":2,"isim":"Büyü Kitabı","fiyat":435}PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/esyalar/4{"hata":"4 numaralı eşya yok"}PS C:\kod\ilk-sunucu> curl.exe http://localhost:3000/esyalar/abc{"hata":"id bir sayı olmalı"}yollar tablosu bu işi yapamaz: /esyalar/1, /esyalar/2, /esyalar/3... her biri ayrı bir anahtar olurdu. Tabloya bakmadan önce yolu kendin incelemelisin.
İpucu 1
Sunucunun fonksiyonunda, tabloya bakmadan önce req.url.startsWith("/esyalar/") ile denetle (Ders 9). Uyuyorsa ayrı bir fonksiyon çağır ve return ile çık.
İpucu 2
req.url.slice("/esyalar/".length) yolun sonundaki id metnini verir. Number ile sayıya çevir, Number.isNaN ile denetle, eşyayı find ile ara (Ders 11).
İpucu 3
Sınır durumu: /esyalar/ için id metni boş kalır ve Number("") sonucu 0'dır, NaN değil. Boş metni ayrıca denetlemezsen bu istek "0 numaralı eşya yok" alır.
Çözümü göster
function esyaGetir(req, res) { const idMetni = req.url.slice("/esyalar/".length) const id = Number(idMetni) if (idMetni === "" || Number.isNaN(id)) { jsonGonder(res, 400, { hata: "id bir sayı olmalı" }) return } const esya = esyalar.find((e) => e.id === id) if (!esya) { jsonGonder(res, 404, { hata: `${id} numaralı eşya yok` }) return } jsonGonder(res, 200, esya)}Sunucunun fonksiyonunda, console.log'un hemen altına:
if (req.method === "GET" && req.url.startsWith("/esyalar/")) { esyaGetir(req, res) return }Sınır değerleriyle sonuçlar:
/esyalar/3→ 200,{"id":3,"isim":"Çevik Botlar","fiyat":300}(son eşya)/esyalar/4→ 404,{"hata":"4 numaralı eşya yok"}(sonun bir fazlası)/esyalar/0→ 404,{"hata":"0 numaralı eşya yok"}/esyalar/-1→ 404,{"hata":"-1 numaralı eşya yok"}/esyalar/abc→ 400,{"hata":"id bir sayı olmalı"}/esyalar/→ 400,{"hata":"id bir sayı olmalı"}
Son satıra dikkat: Görev 1'de /esyalar/ 404 alıyordu, artık 400 alıyor; çünkü startsWith denetimi onu da yakalıyor. Adresten id okuma işini Ders 28'de Express req.params ile tek satırda yapacak.
Eşya ekle
POST /esyalar yolunu ekle. Gövdedeki isim ve fiyat ile yeni bir eşya oluştursun ve 201 ile döndürsün. Şu durumlarda 400 versin: gövde geçerli bir JSON değilse, isim yoksa ya da boşsa, fiyat bir sayı değilse ya da 0'dan küçükse. Bedava eşya (fiyat 0) kabul edilmeli.
Denemek için proje klasöründe esya.json dosyası aç, içeriğini her denemede değiştirip kaydet ve şununla gönder:
PS C:\kod\ilk-sunucu> curl.exe -i --json "@esya.json" http://localhost:3000/esyalarİpucu 1
sampiyonEkle'yi örnek al: gövdeyi govdeyiOku ile oku, JSON.parse'ı try içine koy, her 400'den sonra return yaz. Yeni yolu tabloya eklemeyi unutma.
İpucu 2
typeof deger, değerin türünü metin olarak verir: typeof 500 sonucu "number", typeof "500" sonucu "string". Fiyat denetimi: typeof veri.fiyat !== "number" || veri.fiyat < 0.
İpucu 3
Bedava eşyayı !veri.fiyat ile denetleme: 0 yanlış sayılan bir değerdir (Ders 12) ve !0 sonucu true olur; fiyatı 0 olan eşya yanlışlıkla reddedilir.
Çözümü göster
async function esyaEkle(req, res) { const metin = await govdeyiOku(req) let veri try { veri = JSON.parse(metin) } catch (hata) { jsonGonder(res, 400, { hata: "Gövde geçerli bir JSON değil" }) return } if (!veri?.isim) { jsonGonder(res, 400, { hata: "isim alanı gerekli" }) return } if (typeof veri.fiyat !== "number" || veri.fiyat < 0) { jsonGonder(res, 400, { hata: "fiyat 0 ya da daha büyük bir sayı olmalı" }) return } const yeni = { id: esyalar.length + 1, isim: veri.isim, fiyat: veri.fiyat } esyalar.push(yeni) jsonGonder(res, 201, yeni)}Tabloya "POST /esyalar": esyaEkle satırını ekle. Sunucu yeni başlamışken esya.json'a sırayla yazıp gönderdiğimiz gövdeler ve sonuçları:
{ "isim": "Ölüm Şapkası", "fiyat": 1100 }→ 201,{"id":4,"isim":"Ölüm Şapkası","fiyat":1100}{ "isim": "Bedava Bilet", "fiyat": 0 }→ 201,{"id":5,"isim":"Bedava Bilet","fiyat":0}{ "isim": "Borçlu Kalkan", "fiyat": -1 }→ 400,{"hata":"fiyat 0 ya da daha büyük bir sayı olmalı"}{ "isim": "Metin Fiyat", "fiyat": "500" }→ 400, aynı fiyat hatası{ "isim": "", "fiyat": 100 }→ 400,{"hata":"isim alanı gerekli"}{ "fiyat": 100 }→ 400,{"hata":"isim alanı gerekli"}{ "isim": "Fiyatsız" }→ 400, fiyat hatasıKılıç→ 400,{"hata":"Gövde geçerli bir JSON değil"}null→ 400,{"hata":"isim alanı gerekli"}
Sınırlar tam istediğimiz yerde: 0 geçiyor, -1 geçmiyor; tırnaklı "500" sayı değil, metin. Bir eşya ekledikten sonra GET /esyalar ile listenin uzadığını, GET /esyalar/4 ile yeni eşyanın geldiğini de gör.
Kendini sına
Soru 1
Bu adreste hangi bilgisayar, hangi kapı ve hangi yol isteniyor? http://localhost:5500/index.html
Cevabı göster
Bilgisayar localhost, yani isteği atan bilgisayarın kendisi. Kapı 5500; Live Server'ın alışılmış portu. Yol /index.html. Live Server o kapıyı dinliyorsa dosyayı gönderir; dinlemiyorsa tarayıcı ERR_CONNECTION_REFUSED gösterir.
Soru 2
Hangi adresler sonsuza dek "yükleniyor" kalır?
const sunucu = http.createServer((req, res) => { if (req.url === "/") { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }) res.end("Ana sayfa") } else if (req.url === "/kayit") { res.writeHead(201) }})Cevabı göster
İki tür adres. /kayit dalı başlığı yazıyor ama res.end çağırmıyor; yanıt hiç bitmiyor. Bunun yanında / ve /kayit dışındaki her adres de asılı kalır, çünkü else dalı yok: o isteklere hiçbir yanıt yazılmıyor. Kural: her yoldan mutlaka bir res.end geçmeli; bilinmeyen adresler için de bir else ile 404 gönder.
Soru 3
Sunucunda console.log(req.method, req.url) var. Tarayıcıda localhost:3000/sampiyonlar adresini bir kez açtın. Terminalde hangi satırları görmeyi beklersin?
Cevabı göster
GET /sampiyonlar ve büyük ihtimalle GET /favicon.ico. Tarayıcı, sekme simgesini almak için kendiliğinden ikinci bir istek atar. Adres çubuğu her zaman GET gönderir.
Soru 4
Mesajı oku: hangi kapıda ne oldu, ne yaparsın?
Error: listen EADDRINUSE: address already in use :::5500Cevabı göster
Sunucun 5500 numaralı kapıyı açamadı, çünkü o kapıyı başka bir program zaten dinliyor. En sık sebep: aynı sunucuyu ikinci kez başlattın ve ilki başka bir terminalde hâlâ açık. O terminali bulup Ctrl+C'ye bas ya da sunucuna başka bir port ver. Kimin tuttuğunu bulamazsan Get-NetTCPConnection -LocalPort 5500 ile PID'i öğren, Get-Process -Id ile adına bak ve yalnız adı node ise Stop-Process -Id ile kapat.
Soru 5
govdeyiOku neden bütün parçaları bekliyor? İlk parça gelir gelmez JSON.parse etsek ne olurdu?
Cevabı göster
Büyük bir gövdenin ilk parçası yarım bir JSON'dur; JSON.parse onu okuyamaz ve Unterminated string in JSON ya da Unexpected end of JSON input gibi bir SyntaxError fırlatır. Küçük gövdeler çoğu zaman tek parçada geldiği için hata yalnız büyük isteklerde çıkardı: "bende çalışıyor" ama gerçekte yanlış bir kod.
Soru 6
npm run dev ile çalışan sunucuna POST ile iki şampiyon ekledin, sonra sunucu.js'te bir yazım hatasını düzeltip kaydettin. GET /sampiyonlar kaç şampiyon döndürür?
Cevabı göster
Üç. Kaydedince --watch programı yeniden başlattı; sampiyonlar dizisi koddaki ilk hâline, yani Ahri, Garen ve Jinx'e döndü. POST ile eklenenler yalnız belleğe yazılmıştı. Veriyi kalıcı yapmayı Ders 29'da öğreneceksin.
Soru 7
Bu istek neden 400 aldı? Gövde mi bozuk, yol mu yanlış?
$ curl.exe -i --json "{}" http://localhost:3000/sampiyonlarHTTP/1.1 400 Bad Request...{"hata":"isim alanı gerekli"}Cevabı göster
Yol doğru (POST /sampiyonlar tabloda var) ve gövde geçerli bir JSON: {} boş bir obje. JSON.parse başarılı oldu, ama isim alanı olmadığı için !veri?.isim denetimi 400 döndürdü. Yol yanlış olsaydı 404 alırdın.
Kayıt noktası
Sunucunu Git'e kaydet. ilk-sunucu yeni bir proje klasörü olduğu için önce git init gerekiyor. Sunucuyu durdurmana gerek yok; komutları ikinci terminalde yaz:
Bir sonraki derste aynı API'yi Express ile yazacaksın: elle kurduğun yönlendirme tablosu, gövde okuma ve 404 işleri birkaç satıra inecek, /sampiyonlar/2 gibi adreslerden id okumak da tek satır olacak.