İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

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:http ile bir sunucu yazıp ona tarayıcıdan, curl.exe ile ve Invoke-RestMethod ile istek atabilirsin
  • req.method ve req.url'e bakarak her adrese ayrı yanıt verebilir, bilinmeyen adreslere 404 döndürebilirsin
  • Doğru durum kodu ve Content-Type başlığıyla JSON yanıt gönderebilirsin
  • node --watch ile 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
  1. Sunucu: hiç bitmeyen program
  2. Port ve localhost: bina ve kapı numarası
  3. İlk sunucun
  4. Sunucuyla konuşmak
  5. Durdurmak, yeniden başlatmak ve node --watch
  6. İsteği tanımak: req.method ve req.url
  7. Yönlendirme: her adrese kendi cevabı
  8. Yanıtın etiketi: Content-Type
  9. JSON yanıt ve durum kodu
  10. Yönlendirme tablosu
  11. POST: gelen gövdeyi okumak
  12. Görevler
  13. Kendini sına
  14. 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 Tarayıcı curl.exe istemciler Senin bilgisayarın adı localhost, adresi 127.0.0.1 3000 sunucu.js bekliyor 5500 Live Server 8080 kimse yok
Şekil 27.1 İki istemci de 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ÖrnekAnlamı
protokolhttp://hangi dille konuşulacak: HTTP
makinelocalhosthangi bina: kendi bilgisayarın
port:3000hangi kapı: 3000 numaralı
yol/sampiyonlarkapı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: http için 80, https için 443. https://ddragon.leagueoflegends.com aslı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:

Terminal — PowerShell
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:

sunucu.js — ilk-sunucu — Visual Studio Code
sunucu.js
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}`)})
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\ilk-sunucu> node sunucu.jsSunucu çalışıyor: http://localhost:3000
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Program 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 gelen http modülünü al. Ders 17'deki node:fs/promises gibi; 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.
İstemci tarayıcı ya da curl.exe 1. istek: GET / 2. yanıt: 200 + metin sunucu.js her istekte çalışan fonksiyon: (req, res) => { req gelen isteği okursun res yanıtı yazarsın res.end() gönder, bitir } 3. kapanmaz, sıradakini bekler
Şekil 27.2 Her istek aynı döngüden geçer: istek gelir, fonksiyon çalışır, yanıt gider, sunucu beklemeye döner.

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:

İkinci terminal
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.

Sık hatacurl: (7) Failed to connect to localhost:3000

Ne 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.

Terminal — PowerShell
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:

Terminal — PowerShell
PS C:\kod\ilk-sunucu> node --watch sunucu.jsSunucu çalışıyor: http://localhost:3000

Metni değiştirip kaydet. Terminal temizlenir ve şu üç satır çıkar:

Kaydettikten sonra
Change detected in 'C:\\kod\\ilk-sunucu\\sunucu.js'Restarting 'sunucu.js'Sunucu çalışıyor: http://localhost:3000

Change 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:

package.json
  "scripts": {    "start": "node sunucu.js",    "dev": "node --watch sunucu.js"  },
Terminal — PowerShell
PS C:\kod\ilk-sunucu> npm run dev > ilk-sunucu@1.0.0 dev> node --watch sunucu.js Sunucu çalışıyor: http://localhost:3000

Dersin geri kalanında sunucun hep npm run dev ile çalışsın: kaydet, sunucu kendini yenilesin, sen yalnız isteği tekrarla.

Sık hataError: listen EADDRINUSE: address already in use :::3000

Ne 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.

Terminal — PowerShell
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.0

EADDRINUSE "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:

Terminal — PowerShell
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 29452

OwningProcess, 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:

sunucu.js
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:

İkinci terminal
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 sunucunum

Sunucunun terminalinde her istek için bir satır belirir:

Sunucunun terminali
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/1
  • req.method isteğin yöntemi: GET, POST, DELETE... Tarayıcının adres çubuğu her zaman GET gönderir. curl.exe'de yöntemi -X ile seçersin.
  • req.url adresin makine ve porttan sonraki kısmı. ? ile başlayan sorgu kısmı da onun içinde: /sampiyonlar?rol=buyucu.
  • /favicon.ico isteğ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:

sunucu.js
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")    }})
İkinci terminal
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 yok

res.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.url sorgu 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.

Sık hataİstek hiç bitmiyor: res.end unutuldu

Ne 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.

sunucu.js✗ Yanlış
    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 {
Terminal — PowerShell
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:

sunucu.js
    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:

localhost:3000
Tarayıcı sayfasında Sihirdar Vadisi'ne hoÅŸ geldin yazıyor; ş harfinin yerinde Å ve Ÿ karakterleri var.
Gerçek ekran görüntüsü: Chrome, ş 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:

sunucu.js
    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-TypeNe gönderiyorsun
text/plain; charset=utf-8düz metin
text/html; charset=utf-8HTML sayfası
application/json; charset=utf-8JSON 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:

sunucu.js
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.

İkinci terminal
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:

localhost:3000/sampiyonlar
Tarayıcıda girintisiz, sıkışık JSON: Ahri, Garen ve Jinx; Türkçe harfler doğru. Üstte Okunaklı hale getir yazan boş bir onay kutusu var.
Gerçek ekran görüntüsü. İngilizce Chrome'da kutunun adı Pretty-print.

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

localhost:3000/sampiyonlar
Okunaklı hale getir kutusu işaretli; JSON her alan ayrı satırda, girintili gösteriliyor.
Gerçek ekran görüntüsü: kutu işaretli, her alan kendi satırında.

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ı.

Sık hataTürkçe harfler bozuk: 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.

sunucu.js✗ Yanlış
    } else if (req.method === "GET" && req.url === "/sampiyonlar") {        res.end(JSON.stringify(sampiyonlar))
localhost:3000/sampiyonlar
Tarayıcıda JSON metni; Büyücü, Dövüşçü ve Nişancı sözcüklerindeki Türkçe harfler Ã, Å ve Ä ile başlayan anlamsız karakterlere dönmüş.
Gerçek ekran görüntüsü: başlıksız JSON. Okunaklı hale getir kutusu da yok, çünkü tarayıcı bunun JSON olduğunu bilmiyor.

Invoke-RestMethod de aynı yanıtta Büyücü yerine Büyücü yazar. Denediğimizde iki istemci farklı davrandı:

BaşlıkTarayıcıInvoke-RestMethod (PowerShell 5.1)
hiç yokbozukbozuk
application/jsondoğrubozuk
application/json; charset=utf-8doğrudoğ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:

sunucu.js
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.

yollar "GET /" anaSayfa "GET /sampiyonlar" sampiyonlariListele "POST /sampiyonlar" sampiyonEkle gelen istek POST /sampiyonlar eşleşme yok: undefined 404 yanıtı aranan anahtar: `${req.method} ${req.url}`
Şekil 27.3 Sunucu, isteğin yöntemi ile yolundan bir anahtar kurar ve tabloda arar. Bulursa o fonksiyonu çalıştırır, bulamazsa 404 döner. Üçüncü 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):

parca-deneme.js
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)
Çıktı
<Buffer 7b 22 69 73 69 6d 22 3a 20 22 4a 69>{"isim": "Ji{"isim": "Jinx", "rol": "Nişancı"}Jinx

Ağ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:

parca-deneme.js
const ilkParca = Buffer.from('{"isim": "Ji')const veri = JSON.parse(ilkParca.toString())console.log(veri.isim)
Hata çıktısı
<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.0

Unterminated 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.

ağdan gelen parçalar 1 2 3 for await (const parca of req) her turda bir parçayı bekler, diziye ekler Buffer.concat(parcalar).toString() JSON.parse(metin) geçerli: obje → 201 bozuk: catch → 400 Küçük gövde tek parça, büyük gövde çok parça gelir. Son parça gelmeden JSON okunamaz.
Şekil 27.4 Gövdenin yolculuğu: parçalar tek tek beklenir, birleştirilir, metne çevrilir, sonra JSON olarak okunur.

Gövdeyi okuyan fonksiyon

sunucu.js'e dön. jsonGonder'in hemen altına şu fonksiyonu ekle:

sunucu.js
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.

Sık hataSyntaxError: Unexpected reserved word

Ne zaman çıkar? for await ya da await yazdığın fonksiyonun başına async koymayı unutunca.

sunucu.js✗ Yanlış
function govdeyiOku(req) {    const parcalar = []    for await (const parca of req) {
Sunucunun terminali
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:

sunucu.js
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:

İkinci terminal
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:

sunucu.js
    for await (const parca of req) {        console.log("Parça geldi:", parca.length, "bayt")        parcalar.push(parca)    }
İkinci terminal
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) + '"}')).id5

Dosyayı 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.

Sunucunun terminali
POST /sampiyonlarParça geldi: 35 baytPOST /sampiyonlarParça geldi: 65536 baytParça geldi: 65536 baytParça geldi: 65536 baytParça geldi: 3404 bayt

Lux'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.

Sık hataSyntaxError: Unexpected token 'L', "Lux" is not valid JSON

Ne 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:

İkinci terminal
PS C:\kod\ilk-sunucu> curl.exe -i --json "Lux" http://localhost:3000/sampiyonlarcurl: (56) Recv failure: Connection was reset
Sunucunun terminali
POST /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:

sunucu.js
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 veri try'ın dışında, çünkü bloğun içinde açılan kutu dışarıdan görünmez (Ders 10).
  • catch bozuk metni sunucunun terminaline yazar ve istemciye sebebiyle birlikte 400 döner. hata.message, JSON.parse'ın kendi açıklaması.
  • !veri?.isim hem isim gelmediğinde hem gövde null olduğunda 400 verir; ?. (Ders 12) olmasaydı null gö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:

İkinci terminal
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:

İkinci terminal
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.

Sık hataSunucu {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.

İkinci terminal
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:

Sunucunun terminali
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.

Sık hataError [ERR_HTTP_HEADERS_SENT]: Cannot write headers after they are sent to the client

Ne 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.

sunucu.js✗ Yanlış
    } 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" })
Sunucunun terminali
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:

sunucu.js
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ç.

Görev 1

Eşya listesi

Sunucuya GET /esyalar yolunu ekle. Şu üç eşyayı JSON olarak, 200 ile döndürsün:

Metin
Uzun Kılıç, 350 altınBüyü Kitabı, 435 altınÇevik Botlar, 300 altın

Beklenen:

Terminal — PowerShell
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
sunucu.js
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 }]
JavaScript
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).

Terminal — PowerShell
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.

Görev 2

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
JavaScript
let ziyaret = 0 function sayac(req, res) {    ziyaret = ziyaret + 1    jsonGonder(res, 200, { ziyaret: ziyaret })}

Tabloya "GET /sayac": sayac satırını ekle.

Terminal — PowerShell
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.

Görev 3

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.

altin.js✗ Yanlış
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:

  1. Invoke-RestMethod http://localhost:3001/altin notu Faiz için en az 10 altın gerekir diye yazıyor.
  2. curl.exe --max-time 5 http://localhost:3001/faiz şunu veriyor: curl: (28) Operation timed out after 5012 milliseconds with 0 bytes received.
  3. curl.exe -X POST http://localhost:3001/harca ilk beş seferde 40, 30, 20, 10, 0 döndürüyor. Altıncıda {"hata":"Altın yetmiyor"} geliyor, ama hemen ardından sunucu ERR_HTTP_HEADERS_SENT ile çö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
altin.js
    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 {
Terminal — PowerShell
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.

Görev 4

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:

Terminal — PowerShell
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
JavaScript
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:

JavaScript
    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.

Görev 5

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:

Terminal — PowerShell
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
JavaScript
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?

JavaScript
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?

Hata çıktısı
Error: listen EADDRINUSE: address already in use :::5500
Cevabı 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ış?

Metin
$ 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.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.