Blok 4 · Ders 28
Express ile API
Express ile düzenli bir REST API: route'lar, parametreler, ara katman (middleware), hata yönetimi ve CRUD.
Bu dersin sonunda
- Express'i bir projeye kurup route yazabilir,
res.send,res.jsonveres.statusile yanıt verebilirsin - Adresten
req.paramsvereq.query, gövdedenexpress.json()ilereq.bodyokuyup metni sayıya çevirebilirsin - Bellekteki bir dizi üzerinde listeleyen, getiren, ekleyen, güncelleyen ve silen uç noktaları doğru durum kodlarıyla (200, 201, 204, 400, 404) yazabilirsin
- Kendi kayıt ara katmanını yazıp
next()'in zincirdeki yerini ve route sırasının neden önemli olduğunu açıklayabilirsin - Gelen veriyi doğrulayıp 400 döndürebilir, 404 işleyicisi ve hata yakalayıcıyla her hatayı JSON'a çevirebilirsin
express.staticile aynı sunucudan bir web sayfası sunup API'nicurl.exeveInvoke-RestMethodile baştan sona test edebilirsin
Bu sayfada
- Neden Express?
- Kurulum
- İlk route
- JSON API: res.json ve res.status
- Adresten veri: req.params ve req.query
- Gövdeden veri: req.body ve express.json()
- Ara katman: isteğin geçtiği kapılar
- CRUD: güncellemek ve silmek
- Doğrulama ve 400
- 404 işleyicisi ve hata yakalayıcı
- Statik dosyalar: express.static
- API'yi terminalden test etmek
- Görevler
- Kendini sına
- Kayıt noktası
Ders 27'deki sunucun 69 satırdı ve her işi elle yapıyordu: yönlendirme tablosunu sen kurdun, gövdeyi parça parça sen topladın, 404'ü sen yazdın. Gerçek projelerde bu işler her seferinde baştan yazılmaz; Node dünyasının en yaygın aracı olan Express kullanılır. Bu derste aynı şampiyon API'sini Express ile yeniden kuruyorsun. Dersin sonunda listeleyen, getiren, ekleyen, güncelleyen ve silen, bozuk istekleri doğru durum koduyla geri çeviren, her isteği deftere yazan ve yanında bir web sayfası da sunan bir API'n olacak; hepsini terminalden test etmeyi de öğreneceksin.
Neden Express?
Ders 27'deki sunucu.js'e dön ve hangi satırların gerçekten şampiyonlarla ilgili olduğuna bak. Çoğu değil. Gövdeyi toplayan govdeyiOku, her yanıta başlık yazan jsonGonder, metotla yoldan anahtar kuran tablo: bunlar her API'de aynıdır. Görev 4'te /esyalar/2'den id'yi çıkarmak bile startsWith, slice ve boş metin denetimi istedi.
Express bu ortak işleri hazır veren bir npm paketidir. Altta yine Ders 27'deki node:http çalışır; Express onun üstüne kurulmuş bir iskelettir. Sen yalnız "şu adrese şu istek gelince ne olsun" kısmını yazarsın.
| İş | Ders 27'de (node:http) | Express'te |
|---|---|---|
| Yolu seçmek | yollar tablosu, "GET /sampiyonlar" anahtarı | app.get("/api/sampiyonlar", ...) |
| Adresten id | startsWith ve slice | req.params.id |
Sorgu (?ara=a) | req.url'in içinde, ayırmak sende | req.query.ara |
| Gövde | for await, Buffer.concat, JSON.parse | express.json() ve req.body |
| JSON yanıt | jsonGonder(res, 201, veri) | res.status(201).json(veri) |
| Bilinmeyen adres | else dalında 404 | hazır bir 404, istersen kendi 404'ün |
Bu derste tablonun sağ sütununu satır satır kuracaksın.
Kurulum
Yeni bir proje klasörü aç: terminalde cd C:\kod ile kod klasörüne geç, mkdir sampiyon-api ile klasörü oluştur ve cd sampiyon-api ile içine gir. Sonra package.json'ı oluştur ve Express'i kur (internet bağlantısı gerekir):
PS C:\kod\sampiyon-api> npm init -yWrote to C:\kod\sampiyon-api\package.json: { "name": "sampiyon-api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs"} PS C:\kod\sampiyon-api> npm install express added 65 packages, and audited 66 packages in 2s 28 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesTek paket istedin, 65 paket geldi: Express'in kendi bağımlılıkları ve onların bağımlılıkları. looking for funding satırı bağış bekleyen paketleri sayar; bir şey yapman gerekmez. node_modules klasörü 65 alt klasörle doldu; Git'e girmemesi için hemen .gitignore adında bir dosya aç ve içine tek satır yaz (Ders 14):
node_modules/Klasörü VS Code'da aç (File → Open Folder). package.json'da "type" satırını "module" yap ve scripts kısmını Ders 27'deki iki betikle değiştir:
{ "name": "sampiyon-api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node sunucu.js", "dev": "node --watch sunucu.js" }, "keywords": [], "author": "", "license": "ISC", "type": "module", "dependencies": { "express": "^5.3.0" }}En alttaki dependencies bölümünü npm install kendisi ekledi.
İlk route
Proje klasöründe sunucu.js adında bir dosya aç ve şunu yaz. Soldaki dosya listesinde artık node_modules, .gitignore, package-lock.json, package.json ve sunucu.js duruyor; aşağıdaki pencerede kod sığsın diye listeyi gizledik.
import express from "express" const app = express()const port = 3000 app.get("/", (req, res) => { res.send("Sihirdar Vadisi'ne hoş geldin")}) app.listen(port, () => { console.log(`Sunucu çalışıyor: http://localhost:${port}`)})PS C:\kod\sampiyon-api> npm run dev > sampiyon-api@1.0.0 dev> node --watch sunucu.js Sunucu çalışıyor: http://localhost:3000Ders 27'deki gibi npm run dev: kaydettiğin anda sunucu kendini yeniden başlatır.
Satır satır:
import express from "express": yolun başında./yok, demek ki bu bir paket; Node onunode_modules'ta bulur.const app = express(): Express uygulamasını kurar.app(application, uygulama), bütün route'ların ve ayarların toplandığı obje; Ders 27'dekisunucu'nun yerini alıyor.app.get("/", fonksiyon): "/adresineGETisteği gelince bu fonksiyonu çalıştır." Express'te her route böyle bir satırdır: metodun adı küçük harfle (get,post,patch,delete), yanında yol ve isteği karşılayan işleyici fonksiyon. İşleyici Ders 27'deki gibireqveresalır; Express ikisine de yeni yetenekler eklemiştir.res.send(metin): yanıtı gönderir ve bitirir. Durum kodunu,Content-Type'ı ve gövdenin uzunluğunu kendisi yazar.app.listen(port, fonksiyon): kapıyı açar. Express içeride Ders 27'dekihttp.createServer'ı çağırır.
İkinci bir terminal aç ve isteği at:
PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/HTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: text/html; charset=utf-8Content-Length: 30ETag: W/"1e-D+CzAxorbVcQ0BzxLHrqT1Wjk5k"Date: Sun, 11 Oct 2026 18:00:28 GMTConnection: keep-aliveKeep-Alive: timeout=5 Sihirdar Vadisi'ne hoş geldinTek satır başlık yazmadın, ama yanıtın başı dolu geldi:
Content-Type: text/html; charset=utf-8: Express metni bir HTML parçası saydı ve kodlamasını da yazdı. Ders 27'dekihoÅŸsorunu burada yok.Content-Length: 30: gövde 30 bayt. Cümle boşluklarla birlikte 29 karakter;şUTF-8'de iki bayt tutuyor.ETag: içeriğin parmak izi. Tarayıcı aynı adresi yeniden isterken elindeki parmak izini gönderir; içerik değişmediyse Express gövdeyi tekrar yollamaz, Ders 26'daki304'ü döner.X-Powered-By: Express: sunucu hangi çatıyla yazıldığını söylüyor. Saldırgan için bu da bir ipucu; sunucunun güvenliğini Ders 31'de ele alacaksın.
Tarayıcıda localhost:3000 adresini açarsan aynı cümle görünür. Şimdi tanımlamadığın bir adresi iste.
Cannot GET /yokNe zaman çıkar? Metot ve yol ikilisine uyan bir route olmadığında. Kendi 404'ünü yazana kadar Express bu küçük HTML sayfasını gönderir.
PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/yokHTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Security-Policy: default-src 'none'X-Content-Type-Options: nosniffContent-Type: text/html; charset=utf-8Content-Length: 142Date: Sun, 11 Oct 2026 18:00:28 GMTConnection: keep-aliveKeep-Alive: timeout=5 <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head><body><pre>Cannot GET /yok</pre></body></html>Cannot GET /yok: "/yok adresini GET ile alamıyorum." Metot farklıysa mesaj da değişir: POST attığın bir adresin route'u yoksa Cannot POST /... yazar.
Nasıl düzelir? Önce adresi denetle: yazım, /api öneki, metot. Adres doğruysa o route'u henüz yazmamışsındır. Dersin ilerisinde bu HTML sayfasının yerine JSON döndüren kendi 404 işleyicini yazacaksın.
Ne zaman çıkar? 3000 numaralı kapıyı başka bir program, örneğin hâlâ açık duran Ders 27 sunucun dinlerken. Ders 27'de node:http bu durumda EADDRINUSE ile çöküyordu. Express 5 ise hatayı app.listen'a verdiğin fonksiyona iletir. Fonksiyonun hataya bakmadığı için başarı mesajını yazar ve program sessizce biter:
PS C:\kod\sampiyon-api> npm run dev > sampiyon-api@1.0.0 dev> node --watch sunucu.js Sunucu çalışıyor: http://localhost:3000Completed running 'sunucu.js'. Waiting for file changes before restarting...İpucu son satırda: Completed running "çalışması bitti" demek, oysa bir sunucu hiç bitmemeli. İstekleri kapıyı tutan eski sunucu karşılar: curl.exe http://localhost:3000/ beklediğin cümle yerine Ders 27'nin cevabını, {"mesaj":"Sihirdar Vadisi'ne hoş geldin"} JSON'unu döndürür.
Nasıl düzelir? İki adımda. Önce hatayı görünür yap: Express 5'in belgelerinde önerdiği biçimle app.listen'ın fonksiyonu bir hata parametresi alsın ve hata varsa fırlatsın.
app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Sunucu çalışıyor: http://localhost:${port}`)})Kaydet, npm run dev'i Ctrl+C ile durdur ve sunucuyu bir kez node sunucu.js ile başlat. Kapı hâlâ doluyken artık hata ortada:
PS C:\kod\sampiyon-api> node sunucu.jsfile:///C:/kod/sampiyon-api/sunucu.js:12 throw hata ^ Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:2324:16) ... at file:///C:/kod/sampiyon-api/sunucu.js:10:5 ... { code: 'EADDRINUSE', errno: -4091, syscall: 'listen', address: '::', port: 3000} Node.js v24.20.0Sonra kapıyı tutan eski sunucuyu kapat (Ders 27'deki Get-NetTCPConnection yolu) ve npm run dev'i yeniden başlat. Bu dersteki bütün kodlarda app.listen artık bu biçimde.
JSON API: res.json ve res.status
Şampiyon listesini ekle: diziyi const port'un altına, yeni route'u app.get("/")'in altına yaz.
let sampiyonlar = [ { id: 1, isim: "Ahri", rol: "Büyücü" }, { id: 2, isim: "Garen", rol: "Dövüşçü" }, { id: 3, isim: "Jinx", rol: "Nişancı" }] app.get("/", (req, res) => { res.send("Sihirdar Vadisi'ne hoş geldin")}) app.get("/api/sampiyonlar", (req, res) => { res.json(sampiyonlar)})let, çünkü silerken dizinin yerinefilter'ın verdiği yeni diziyi koyacağız (Ders 23'teki maç günlüğü gibi)./api/sampiyonlar: API adreslerini/apiile başlatıyoruz. Dersin sonunda aynı sunucu bir web sayfası da sunacak; önek, sayfa adresleriyle API adreslerinin karışmasını önler. Yaygın bir gelenek.res.json(veri): Ders 27'dekijsonGonder'in işini görür. VeriyiJSON.stringifyile metne çevirir,Content-Type: application/json; charset=utf-8yazar ve gönderir.
PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/api/sampiyonlarHTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 128ETag: W/"80-vFMJ8H5lzGH6NdcJX5UCrDd7RM4"Date: Sun, 11 Oct 2026 18:03:39 GMTConnection: keep-aliveKeep-Alive: timeout=5 [{"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\sampiyon-api> Invoke-RestMethod http://localhost:3000/api/sampiyonlar id isim rol-- ---- --- 1 Ahri Büyücü 2 Garen Dövüşçü 3 Jinx NişancıBaşka bir durum kodu göndermek için yanıttan önce res.status(kod) yazarsın: res.status(404).json({ hata: "..." }). res.status kodu ayarlar ve res'in kendisini geri verir; bu yüzden arkasına hemen .json(...) eklenebilir. Ders 9'daki zincirlemenin aynısı.
Bir fark daha dene: curl.exe http://localhost:3000/API/Sampiyonlar/ da aynı listeyi döndürür. Express varsayılan olarak büyük-küçük harfe ve sondaki /'e bakmaz; Ders 27'deki tablonda ikisi de 404 alıyordu.
Adresten veri: req.params ve req.query
İstemci veriyi sunucuya çoğunlukla üç yerde taşır: ikisi adreste, biri gövdede. Express her birini req'in ayrı bir alanına koyar.
Yol parametresi: req.params
Tek bir şampiyonu getirmek için /api/sampiyonlar/1, /api/sampiyonlar/2... diye sonsuz route yazamazsın. Yolun değişen kısmına iki noktayla bir ad verirsin. /api/sampiyonlar route'unun altına ekle; console.log satırları geçici:
app.get("/api/sampiyonlar/:id", (req, res) => { console.log(req.params) console.log(typeof req.params.id) res.json({ istenen: req.params.id })})PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/2{"istenen":"2"}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/Ahri{"istenen":"Ahri"}[Object: null prototype] { id: '2' }string[Object: null prototype] { id: 'Ahri' }string:idbir yer tutucu./api/sampiyonlar/ile başlayıp arkasından tek parça gelen her adres bu route'a uyar; o parçareq.params.id'ye konur.[Object: null prototype]: Express 5 bu objeyi "prototipsiz" kurar; bir güvenlik önlemi. Senin için sıradan bir obje gibi çalışır, bu yazıyı görmezden gel.typeofstringdedi: adresten gelen her şey metindir.2bile"2"olarak gelir.
console.log satırlarını sil ve route'u gerçek işine kavuştur:
app.get("/api/sampiyonlar/:id", (req, res) => { const id = Number(req.params.id) if (!Number.isInteger(id)) { return res.status(400).json({ hata: "id bir tam sayı olmalı" }) } const sampiyon = sampiyonlar.find((s) => s.id === id) if (!sampiyon) { return res.status(404).json({ hata: `${id} numaralı şampiyon yok` }) } res.json(sampiyon)})Numbermetni sayıya çevirir;Number.isInteger(Ders 17) "tam sayı mı?" diye sorar.abc(NaN) ve2.5geçemez:400, çünkü hatalı olan isteğin kendisi.find(Ders 11) şampiyonu arar; bulamazsaundefineddöner ve cevap404olur.return res.status(...).json(...): yanıtı gönderir ve fonksiyondan aynı satırda çıkar. Ders 27'dejsonGonder(...)vereturn'ü ayrı satırlara yazıyordun.return'ü unutursan fonksiyon aşağı devam eder ve aynı isteğe ikinci bir yanıt göndermeye kalkar; bunun bedelini doğrulama bölümünde göreceksin.
PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/2{"id":2,"isim":"Garen","rol":"Dövüşçü"}PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/api/sampiyonlar/9HTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 36ETag: W/"24-WSS7mtlwsKE192gyASQV02ol7hQ"Date: Sun, 11 Oct 2026 18:07:36 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"9 numaralı şampiyon yok"}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/abc{"hata":"id bir tam sayı olmalı"}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/2.5{"hata":"id bir tam sayı olmalı"}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/0{"hata":"0 numaralı şampiyon yok"}Ne zaman çıkar? req.params.id'yi sayıya çevirmeden === ile karşılaştırınca. Hata mesajı yok; sunucu "çalışıyor", ama hiçbir id'yi bulamıyor.
const sampiyon = sampiyonlar.find((s) => s.id === req.params.id)PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/api/sampiyonlar/2HTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 36ETag: W/"24-OewpNrLFB8l54siHjh9en0QmY1g"Date: Sun, 11 Oct 2026 18:22:14 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"2 numaralı şampiyon yok"}Dizideki id sayı (2), adresten gelen metin ("2"). === türlere de baktığı için (Ders 2) eşleşme hiç olmaz.
Nasıl düzelir? Adresten geleni önce sayıya çevir: const id = Number(req.params.id), sonra s.id === id. Sorgu dizesindeki değerler için de aynı kural geçerli.
Sorgu dizesi: req.query
Listeyi süzmenin REST'teki yeri sorgu dizesidir: /api/sampiyonlar?ara=a. Express sorgu kısmını yoldan ayırıp req.query objesine koyar. Önce neye benzediğine bak; liste route'una geçici bir satır ekle:
app.get("/api/sampiyonlar", (req, res) => { console.log(req.query) res.json(sampiyonlar)})Üç istek at: ?ara=a, ?ara=a&rol=Tank ve ?ara=a&ara=e. Adresi her seferinde tırnak içinde yaz: curl.exe "http://localhost:3000/api/sampiyonlar?ara=a&rol=Tank". Üçü de şimdilik bütün listeyi döndürür, ama sunucunun terminali farklı:
[Object: null prototype] { ara: 'a' }[Object: null prototype] { ara: 'a', rol: 'Tank' }[Object: null prototype] { ara: [ 'a', 'e' ] }Her değer metin. Aynı ad iki kez gelirse dizi olur; hiç gelmezse req.query.ara undefined olur. Ders 27'nin aksine yol hâlâ eşleşti: Express route ararken sorgu kısmına bakmaz. Satırı sil ve gerçek aramayı yaz:
app.get("/api/sampiyonlar", (req, res) => { const { ara } = req.query if (typeof ara !== "string") { return res.json(sampiyonlar) } const aranan = ara.toLowerCase() res.json(sampiyonlar.filter((s) => s.isim.toLowerCase().includes(aranan)))})const { ara } = req.query: parçalama.typeof ara !== "string":arahiç yoksa (undefined) ya da iki kez gelip dizi olduysa bütün liste döner. Dışarıdan gelen değerin türünü varsayma, sor.toLowerCaseveincludes(Ders 9) büyük-küçük harfe bakmadan arar;filter(Ders 11) uyanları toplar.
PS C:\kod\sampiyon-api> curl.exe "http://localhost:3000/api/sampiyonlar?ara=a"[{"id":1,"isim":"Ahri","rol":"Büyücü"},{"id":2,"isim":"Garen","rol":"Dövüşçü"}]PS C:\kod\sampiyon-api> curl.exe "http://localhost:3000/api/sampiyonlar?ara=X"[{"id":3,"isim":"Jinx","rol":"Nişancı"}]PS C:\kod\sampiyon-api> curl.exe "http://localhost:3000/api/sampiyonlar?ara=zz"[]PS C:\kod\sampiyon-api> curl.exe "http://localhost:3000/api/sampiyonlar?ara="[{"id":1,"isim":"Ahri","rol":"Büyücü"},{"id":2,"isim":"Garen","rol":"Dövüşçü"},{"id":3,"isim":"Jinx","rol":"Nişancı"}]Adresi tırnak içine aldık, çünkü PowerShell'de & özel bir işarettir (Ders 26). Hiçbir şey bulunamayınca 404 değil, boş dizi döndü: liste adresi var, yalnızca içi boş. Boş arama (?ara=) her şeyi getirdi, çünkü boş metin her adın içinde geçer.
Gövdeden veri: req.body ve express.json()
Yeni şampiyon eklemek için istemci veriyi gövdede gönderir. Ders 27'de gövdeyi for await ile parça parça toplamıştın. Express'te gövdenin req.body'de durması gerekiyor; önce gerçekten durup durmadığına bak. :id route'unun altına yaz:
app.post("/api/sampiyonlar", (req, res) => { const yeni = { id: sampiyonlar.length + 1, isim: req.body.isim, rol: req.body.rol } sampiyonlar.push(yeni) res.status(201).json(yeni)})Yeni kayıt için 201 Created (Ders 26). İsteği at:
TypeError: Cannot read properties of undefined (reading 'isim')Ne zaman çıkar? express.json() olmadan req.body'den bir alan okumaya kalkınca. Express gövdeyi kendiliğinden okumaz; okuyacak ara katmanı eklemediysen req.body undefined kalır.
PS C:\kod\sampiyon-api> curl.exe -i --json "{}" http://localhost:3000/api/sampiyonlarHTTP/1.1 500 Internal Server ErrorX-Powered-By: ExpressContent-Security-Policy: default-src 'none'X-Content-Type-Options: nosniffContent-Type: text/html; charset=utf-8... <!DOCTYPE html>...<pre>TypeError: Cannot read properties of undefined (reading 'isim')<br> at file:///C:/kod/sampiyon-api/sunucu.js:38:63<br> ...</pre>...TypeError: Cannot read properties of undefined (reading 'isim') at file:///C:/kod/sampiyon-api/sunucu.js:38:63 at Layer.handleRequest (C:\kod\sampiyon-api\node_modules\router\lib\layer.js:152:17) ...sunucu.js:38:63 tam olarak req.body.isim'i gösteriyor: undefined'ın isim'i okunamaz. Sunucu çökmedi. Express hatayı yakaladı, terminale yazdı ve istemciye 500 ile yığın izli bir HTML sayfası gönderdi. Aynı hata Ders 27'deki sunucunu düşürürdü.
Nasıl düzelir? Gövdeyi okuyan ara katmanı ekle: app.use(express.json()).
Dizinin hemen altına, route'ların üstüne tek satır ekle:
] app.use(express.json()) app.get("/", (req, res) => {express.json(), isteğin Content-Type'ı application/json ise gövdeyi okur, JSON.parse eder ve sonucu req.body'ye koyar: Ders 27'deki govdeyiOku ile JSON.parse'ın işi tek satırda. app.use "bunu her istekte çalıştır" demek; ne olduğunu bir sonraki bölümde açacağız. Kaydet ve yeniden dene:
PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/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\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/4{"id":4,"isim":"Lux","rol":"Büyücü"}Çalışıyor. Peki çalışmaması gereken bir istek? Boş bir obje gönder:
PS C:\kod\sampiyon-api> curl.exe -i --json "{}" http://localhost:3000/api/sampiyonlarHTTP/1.1 201 CreatedX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 8ETag: W/"8-4U72Xq+/t/P6Xscj7oQblSpGEbo"Date: Sun, 11 Oct 2026 18:08:11 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"id":5}PS C:\kod\sampiyon-api> Invoke-RestMethod http://localhost:3000/api/sampiyonlar id isim rol-- ---- --- 1 Ahri Büyücü 2 Garen Dövüşçü 3 Jinx Nişancı 4 Lux Büyücü 5Sunucu adı ve rolü olmayan bir şampiyonu 201 ile kabul etti. isim ve rol undefined olduğu için JSON.stringify onları yazmadı; geriye yalnız {"id":5} kaldı. Kod çalışıyor ama doğru değil. Bunu doğrulama bölümünde düzelteceksin.
Bozuk bir JSON gönderirsen (curl.exe -i --json "Lux" http://localhost:3000/api/sampiyonlar) express.json() onu okuyamaz ve Express 400 Bad Request ile yine yığın izli bir HTML sayfası döndürür. Sunucu çökmez; bu sayfayı da birazdan JSON'a çevireceksin.
Ara katman: isteğin geçtiği kapılar
app.use(express.json()) satırını açalım. Express'te gelen her istek, sırayla dizilmiş fonksiyonlardan geçer. Her fonksiyon isteğe bakar ve iki şeyden birini yapar: ya yanıtı gönderip işi bitirir ya da next() ile isteği sıradakine devreder. Bu fonksiyonlara ara katman (middleware) denir. express.json() gövdeyi okuyup req.body'ye koyan ve next() diyen bir ara katman; route'ların işleyicileri ise yanıtı gönderen son halkalar.
Kendi ara katmanını yaz: her isteği terminale yazan bir kayıt (log) tutucu. app.use(express.json())'ın hemen üstüne:
app.use((req, res, next) => { console.log(`${req.method} ${req.url}`) next()})app.use(express.json())next, Express'in fonksiyonuna verdiği üçüncü parametre: çağırınca istek bir sonraki halkaya geçer. app.use'a yol vermediğin için bu ara katman her metotta ve her adreste çalışır. Dört istek at (/api/sampiyonlar, /api/sampiyonlar/2, /api/sampiyonlar?ara=x ve /yok) ve sunucunun terminaline bak:
GET /api/sampiyonlarGET /api/sampiyonlar/2GET /api/sampiyonlar?ara=xGET /yokVar olmayan /yok bile deftere girdi: kayıt tutucu zincirin başında, route'lardan önce çalışıyor. Express ara katmanları ve route'ları yazıldıkları sırayla dener. İsteğin dersin sonundaki sunucuda izleyeceği yol:
next() der. Hatalar zincirin dışına çıkıp hata yakalayıcıya gider.Şekildeki 3. ve 5. halkayı, hata yakalayıcıyla birlikte dersin ilerisinde ekleyeceksin.
Kayda durum kodunu eklemek
Bu kaydın bir eksiği var: istek geldiği anda yanıtın ne olacağı henüz belli değil. Yanıt bittiğinde yazmak için res.on("finish", fonksiyon) kullan. Ders 22'deki addEventListener'ın Node'daki karşılığı: "yanıt gönderilip bitince bunu çalıştır". Kayıt tutucuyu şöyle değiştir:
app.use((req, res, next) => { const baslangic = Date.now() res.on("finish", () => { const sure = Date.now() - baslangic console.log(`${req.method} ${req.url} ${res.statusCode} ${sure} ms`) }) next()})GET /api/sampiyonlar 200 5 msGET /api/sampiyonlar/9 404 1 msGET /api/sampiyonlar/abc 400 1 msPOST /api/sampiyonlar 201 13 msGET /yok 404 1 msDate.now() (Ders 23) ile istek gelince saati aldık, yanıt bitince farkı yazdık; milisaniyeler sende farklı çıkar. Gerçek sunucuların kayıtları da böyle satırlardan oluşur: hangi istek, hangi sonuç, ne kadar sürdü. Bir sorun çıktığında ilk bakacağın yer burası.
next() unutulduNe zaman çıkar? Bir ara katman ne yanıt gönderir ne de next() derse. İstek o halkada takılı kalır; arkasındaki route'lar hiç çalışmaz.
app.use((req, res, next) => { console.log(`${req.method} ${req.url}`)})PS C:\kod\sampiyon-api> curl.exe --max-time 5 http://localhost:3000/api/sampiyonlarcurl: (28) Operation timed out after 5014 milliseconds with 0 bytes receivedPS C:\kod\sampiyon-api> Invoke-RestMethod http://localhost:3000/api/sampiyonlar -TimeoutSec 5Invoke-RestMethod : The operation has timed out....İki mesaj da Türkçe Windows'ta da İngilizce çıkar: "işlem zaman aşımına uğradı". Sunucunun terminalinde hata yok, yalnız GET /api/sampiyonlar satırları var: istek geldi, deftere yazıldı ve orada kaldı. Belirti, Ders 27'de res.end'i unuttuğundakinin aynısı.
Nasıl düzelir? Her ara katman ya bir yanıt göndermeli ya da next() demeli. Yanıt gönderen bir dalda next() yazma; ikisini birden yaparsan aynı isteğe iki yanıt gider.
CRUD: güncellemek ve silmek
Elindeki API bir kaynağı listeliyor, getiriyor ve ekliyor. Eksik iki iş var: güncellemek ve silmek. Bu dört işin ortak bir adı var.
| İş | Metot ve adres | Başarıda | Olası hatalar |
|---|---|---|---|
| Listele | GET /api/sampiyonlar | 200 ve dizi | yok; bulunamayınca boş dizi |
| Getir | GET /api/sampiyonlar/:id | 200 ve kayıt | 400 id tam sayı değil, 404 yok |
| Ekle | POST /api/sampiyonlar | 201 ve yeni kayıt | 400 gövde geçersiz |
| Güncelle | PATCH /api/sampiyonlar/:id | 200 ve güncel kayıt | 400, 404 |
| Sil | DELETE /api/sampiyonlar/:id | 204, gövde yok | 400, 404 |
Güncellemek ve silmek de tek bir şampiyonla çalışır: id'yi sayıya çevirmek, şampiyonu aramak, yoksa 404... GET /:id'deki satırların aynısı. Üç route'a üç kez yazmak yerine bu işi bir ara katmana taşı. GET /:id route'unu sil ve yerine şunu yaz:
function sampiyonuBul(req, res, next) { const id = Number(req.params.id) if (!Number.isInteger(id)) { return res.status(400).json({ hata: "id bir tam sayı olmalı" }) } const sampiyon = sampiyonlar.find((s) => s.id === id) if (!sampiyon) { return res.status(404).json({ hata: `${id} numaralı şampiyon yok` }) } req.sampiyon = sampiyon next()} app.get("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { res.json(req.sampiyon)})sampiyonuBul, eski route'un gövdesinin neredeyse aynısı; değişen tek şey sonu. Yanıt göndermek yerine bulduğu şampiyonu req.sampiyon'a koyup next() diyor. express.json()'ın req.body'yi doldurması gibi, bu ara katman da req.sampiyon'u dolduruyor. app.get(yol, sampiyonuBul, işleyici) "önce sampiyonuBul, o next() derse işleyici" demek; 400 ya da 404 durumunda işleyici hiç çalışmaz. Şimdi POST'un altına güncellemeyi ve silmeyi ekle:
app.patch("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { req.sampiyon.isim = req.body?.isim ?? req.sampiyon.isim req.sampiyon.rol = req.body?.rol ?? req.sampiyon.rol res.json(req.sampiyon)}) app.delete("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { sampiyonlar = sampiyonlar.filter((s) => s.id !== req.sampiyon.id) res.status(204).end()})req.body?.isim ?? req.sampiyon.isim: gövdedeisimvarsa onu, yoksa eskisini al (Ders 12). Gövde hiç yoksa?.çökmeyi önler.PATCHyalnız gönderilen alanları değiştirir (Ders 26).req.sampiyondizideki objenin kendisi, bir kopyası değil (Ders 12). Onu değiştirince dizideki kayıt da değişir.res.status(204).end():204 No Content, gövdesiz yanıt.end()gönderip bitirir.
PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/sampiyonlar/1 -ContentType "application/json; charset=utf-8" -Body '{"rol": "Destek"}' id isim rol-- ---- --- 1 Ahri Destek PS C:\kod\sampiyon-api> curl.exe -i -X DELETE http://localhost:3000/api/sampiyonlar/2HTTP/1.1 204 No ContentX-Powered-By: ExpressDate: Sun, 11 Oct 2026 18:10:16 GMTConnection: keep-aliveKeep-Alive: timeout=5 PS C:\kod\sampiyon-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/2{"hata":"2 numaralı şampiyon yok"}204'te Content-Type ve Content-Length bile yok; gönderilecek bir şey yok. Aynı silme ikinci kez 404 aldı: Garen artık yok.
Çalışıyor, ama doğru mu?
Garen'i sildikten sonra bir şampiyon ekle ve listeye bak:
PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Lux", "rol": "Büyücü"}' id isim rol-- ---- --- 3 Lux Büyücü PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri","rol":"Destek"},{"id":3,"isim":"Jinx","rol":"Nişancı"},{"id":3,"isim":"Lux","rol":"Büyücü"}]PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/3{"id":3,"isim":"Jinx","rol":"Nişancı"}PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Delete -Uri http://localhost:3000/api/sampiyonlar/3 PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri","rol":"Destek"}]İki şampiyonun kimliği aynı: 3. Dizide iki eleman kalmıştı, sampiyonlar.length + 1 Lux'a 3 verdi; ama 3 Jinx'indi. find ilk bulduğunu döndürdüğü için Lux'a artık ulaşılamıyor. Jinx'i silmek isteyen ise ikisini birden sildi, çünkü filter id'si 3 olan herkesi eler.
Çare, son verilen kimliği ayrı bir kutuda tutmak. Dizinin altına let sonId = 3 satırını ekle ve POST'u şöyle değiştir:
app.post("/api/sampiyonlar", (req, res) => { sonId = sonId + 1 const yeni = { id: sonId, isim: req.body.isim, rol: req.body.rol } sampiyonlar.push(yeni) res.status(201).json(yeni)})PS C:\kod\sampiyon-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/2PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/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\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri","rol":"Büyücü"},{"id":3,"isim":"Jinx","rol":"Nişancı"},{"id":4,"isim":"Lux","rol":"Büyücü"}]Kural: bir kimlik bir kez verilir; sahibi silinse bile yeniden kullanılmaz. Blok 5'teki veritabanları da kimlikleri böyle sayar.
GET /api/sampiyonlar/yeni 400 alıyor: route sırasıNe zaman çıkar? Sabit bir yolu (/yeni), aynı yeri tutan bir yol parametresinin (/:id) altına yazınca. Diyelim ki son eklenen şampiyonu getiren bir uç nokta yazdın ve onu dosyanın sonuna, app.listen'ın üstüne ekledin:
app.get("/api/sampiyonlar/yeni", (req, res) => { if (sampiyonlar.length === 0) { return res.status(404).json({ hata: "Hiç şampiyon yok" }) } res.json(sampiyonlar[sampiyonlar.length - 1])})PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/api/sampiyonlar/yeniHTTP/1.1 400 Bad RequestX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 35ETag: W/"23-gtdj5v6Jadv3BG5qf5783lhsjWw"Date: Sun, 11 Oct 2026 18:10:38 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"id bir tam sayı olmalı"}Yanıt /yeni route'undan değil, sampiyonuBul'dan geldi. Express route'ları yukarıdan aşağı dener ve ilk uyanı çalıştırır. /api/sampiyonlar/:id daha yukarıda ve yeni sözcüğü de :id'ye uyuyor: req.params.id "yeni" oldu, sayıya çevrilemedi.
Nasıl düzelir? /yeni route'unu /api/sampiyonlar/:id'nin üstüne taşı; en iyisi liste route'unun hemen altı. Sabit yollar önce, yol parametreli yollar sonra. Kaydedince sunucu yeniden başlar ve liste ilk üç şampiyona döner; bu yüzden aynı istek şimdi {"id":3,"isim":"Jinx","rol":"Nişancı"} döndürür. Lux'u yeniden eklersen {"id":4,"isim":"Lux","rol":"Büyücü"} gelir.
Doğrulama ve 400
Kimlik sorunu çözüldü, ama {} hâlâ adsız bir şampiyon ekliyor. Ders 23'te formdaki required niteliğinin F12 ile birkaç saniyede silinebildiğini görmüştün. Sunucu tarayıcıdaki denetime güvenemez: kuralları kendisi yeniden denetler ve uymayan isteği 400 ile geri çevirir. Bunun adını Ders 13'ten biliyorsun: girdi doğrulama.
Kuralları route'un içine gömmek yerine ayrı bir modüle koy; hem POST hem PATCH kullanacak. Proje klasöründe dogrula.js aç. Altına dört deneme satırı ekledik; çalıştırıp sonucu gör:
const roller = ["Büyücü", "Dövüşçü", "Nişancı", "Suikastçı", "Destek", "Tank"] export function sampiyonuDogrula(veri) { if (typeof veri !== "object" || veri === null) { return ["Gövde bir JSON objesi olmalı"] } const hatalar = [] if (typeof veri.isim !== "string" || veri.isim.trim() === "") { hatalar.push("isim boş olmayan bir metin olmalı") } else if (veri.isim.trim().length > 20) { hatalar.push("isim en çok 20 karakter olabilir") } if (!roller.includes(veri.rol)) { hatalar.push(`rol şunlardan biri olmalı: ${roller.join(", ")}`) } return hatalar} console.log(sampiyonuDogrula({ isim: "Lux", rol: "Büyücü" }))console.log(sampiyonuDogrula({ isim: " ", rol: "Aşçı" }))console.log(sampiyonuDogrula({ isim: "Yirmi Bir Karakterlik", rol: "Tank" }))console.log(sampiyonuDogrula(undefined))[][ 'isim boş olmayan bir metin olmalı', 'rol şunlardan biri olmalı: Büyücü, Dövüşçü, Nişancı, Suikastçı, Destek, Tank'][ 'isim en çok 20 karakter olabilir' ][ 'Gövde bir JSON objesi olmalı' ]- Fonksiyon hata mesajlarından oluşan bir dizi döndürüyor; boş dizi "sorun yok" demek. Bütün sorunları birden döndürmek, istemcinin eksiklerini tek istekte görmesini sağlar.
- İlk denetim gövdenin bir obje olduğundan emin olur; gövde yoksa (
undefined) ya danullise ötekilere hiç geçmez. Ötekilerveri.isimokuduğu için bu sıra önemli. trim(Ders 9) yalnız boşluktan oluşan adı da boş sayar. Rol, izin verilen listeden biri olmalı.
node dogrula.js ile çalıştır, sonra alttaki dört console.log satırını sil. sunucu.js'in en üstüne içe aktarmayı ekle:
import express from "express"import { sampiyonuDogrula } from "./dogrula.js"POST ve PATCH'i fonksiyonu kullanacak biçimde değiştir:
app.post("/api/sampiyonlar", (req, res) => { const hatalar = sampiyonuDogrula(req.body) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz şampiyon", ayrintilar: hatalar }) } sonId = sonId + 1 const yeni = { id: sonId, isim: req.body.isim.trim(), rol: req.body.rol } sampiyonlar.push(yeni) res.status(201).json(yeni)}) app.patch("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { const guncel = { isim: req.body?.isim ?? req.sampiyon.isim, rol: req.body?.rol ?? req.sampiyon.rol } const hatalar = sampiyonuDogrula(guncel) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz şampiyon", ayrintilar: hatalar }) } req.sampiyon.isim = guncel.isim.trim() req.sampiyon.rol = guncel.rol res.json(req.sampiyon)})PATCH önce şampiyonun güncellenmiş hâlini guncel adlı yeni bir objede kurar, onu POST'un kurallarıyla denetler ve ancak geçerse kaydı değiştirir. Böylece tek bir alanı bozuk bir istek, kaydın yarısını değiştirip yarısında kalamaz. Gövdeden yalnız isim ve rol alınıyor; başka alanlar, örneğin bir id, sessizce yok sayılır.
PS C:\kod\sampiyon-api> curl.exe -i --json "{}" http://localhost:3000/api/sampiyonlarHTTP/1.1 400 Bad RequestX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 176ETag: W/"b0-4RGaJgG80pjWgiwh0u/qBPFK8d4"Date: Sun, 11 Oct 2026 18:28:25 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Geçersiz şampiyon","ayrintilar":["isim boş olmayan bir metin olmalı","rol şunlardan biri olmalı: Büyücü, Dövüşçü, Nişancı, Suikastçı, Destek, Tank"]}PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -Body '{"isim": "Teemo", "rol": "Nişancı"}'Invoke-RestMethod : {"hata":"Geçersiz şampiyon","ayrintilar":["Gövde bir JSON objesi olmalı"]}...PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Yirmi Karakterlik Ad", "rol": "Tank"}' id isim rol-- ---- --- 4 Yirmi Karakterlik Ad Tank PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Yirmi Bir Karakterlik", "rol": "Tank"}'Invoke-RestMethod : {"hata":"Geçersiz şampiyon","ayrintilar":["isim en çok 20 karakter olabilir"]}...PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/sampiyonlar/1 -ContentType "application/json; charset=utf-8" -Body '{"isim": ""}'Invoke-RestMethod : {"hata":"Geçersiz şampiyon","ayrintilar":["isim boş olmayan bir metin olmalı"]}...PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/sampiyonlar/1 -ContentType "application/json; charset=utf-8" -Body '{"id": 99, "rol": "Destek"}' id isim rol-- ---- --- 1 Ahri DestekSınırlar yerinde: 20 karakter geçti, 21 geçmedi. -ContentType'ı unutan istek artık 500 değil, ne yapması gerektiğini söyleyen bir 400 aldı. Invoke-RestMethod hata yanıtlarında gövdeyi ilk satıra yazar (Ders 26). Express yanıtları Content-Length taşıdığı için, Ders 27'deki sunucunun aksine, bu gövde burada görünüyor.
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the clientNe zaman çıkar? Bir isteğe yanıt gönderdikten sonra ikinci kez göndermeye kalkınca. En sık sebep: 400'ü gönderen satırdaki return'ü unutmak.
app.post("/api/sampiyonlar", (req, res) => { const hatalar = sampiyonuDogrula(req.body) if (hatalar.length > 0) { res.status(400).json({ hata: "Geçersiz şampiyon", ayrintilar: hatalar }) } sonId = sonId + 1 const yeni = { id: sonId, isim: req.body.isim.trim(), rol: req.body.rol } sampiyonlar.push(yeni) res.status(201).json(yeni)})PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Vi", "rol": "Aşçı"}'Invoke-RestMethod : {"hata":"Geçersiz şampiyon","ayrintilar":["rol şunlardan biri olmalı: Büyücü, Dövüşçü, Nişancı, Suikastçı, Destek, Tank"]}...PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri","rol":"Büyücü"},{"id":2,"isim":"Garen","rol":"Dövüşçü"},{"id":3,"isim":"Jinx","rol":"Nişancı"},{"id":4,"isim":"Vi","rol":"Aşçı"}]POST /api/sampiyonlar 201 19 msError [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client at ServerResponse.setHeader (node:_http_outgoing:684:11) ... at file:///C:/kod/sampiyon-api/sunucu.js:69:21 ... { code: 'ERR_HTTP_HEADERS_SENT'}İstemci 400 aldı; her şey yolunda görünüyor. Ama fonksiyon durmadı: geçersiz rollü Vi listeye eklendi ve sunucu.js:69'daki ikinci yanıt "başlıklar gönderildikten sonra yeniden yazılamaz" hatasına çarptı. Kayıt satırı bile 201 diyor, çünkü ikinci res.status(201) durum kodunu değiştirmişti. Sunucu bu kez çökmedi (Express hatayı yakaladı), ama veri bozuldu. Hatanın en kötüsü, kimsenin fark etmediğidir.
Nasıl düzelir? Yanıt gönderen her dalın başına return koy: return res.status(400).json(...). Kural Ders 27'dekiyle aynı: bir istek, bir yanıt.
404 işleyicisi ve hata yakalayıcı
İki tür yanıtın hâlâ JSON değil. Bilinmeyen adreslere Cannot GET sayfası gidiyor; bozuk gövdeye ve koddaki hatalara yığın izli HTML. API'ni kullanan program HTML beklemiyor; üstelik yığın izi, sunucundaki dosya yollarını isteyen herkese gösteriyor. İkisini de zincirin sonuna iki ara katman koyarak düzeltirsin.
404 işleyicisi
Bütün route'ların altına, app.listen'ın üstüne yaz:
app.use((req, res) => { res.status(404).json({ hata: `${req.method} ${req.url} diye bir yol yok` })})Yol vermediğin için her isteğe uyar, ama zincirin sonunda durduğu için yalnız hiçbir route'un yanıtlamadığı istekler buraya ulaşır. next parametresine gerek yok: işi her zaman yanıt göndermek.
PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/yokHTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 36ETag: W/"24-Yk1YQavXOxV3xkZq9K3h0LCf0JE"Date: Sun, 11 Oct 2026 18:11:51 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"GET /yok diye bir yol yok"}PS C:\kod\sampiyon-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar{"hata":"DELETE /api/sampiyonlar diye bir yol yok"}PathError [TypeError]: Missing parameter name at index 1: *Ne zaman çıkar? İnternetteki Express 4 örneklerinde 404 için sık görülen app.get("*", ...), app.all("*", ...) ya da app.use("*", ...) satırını Express 5'e yazınca. Sunucu daha açılmadan çöker:
app.all("*", (req, res) => { res.status(404).json({ hata: `${req.method} ${req.url} diye bir yol yok` })})PS C:\kod\sampiyon-api> node sunucu.jsC:\kod\sampiyon-api\node_modules\path-to-regexp\dist\index.js:108 throw new PathError(`Missing parameter name at index ${index}`, str); ^ PathError [TypeError]: Missing parameter name at index 1: *; visit https://git.new/pathToRegexpError for info at consumeUntil (C:\kod\sampiyon-api\node_modules\path-to-regexp\dist\index.js:108:27) at parse (C:\kod\sampiyon-api\node_modules\path-to-regexp\dist\index.js:140:26) ... at app.all (C:\kod\sampiyon-api\node_modules\express\lib\application.js:495:20) { originalPath: '*'} Node.js v24.20.0Express 5 yol yazımını değiştirdi: * artık tek başına kullanılamaz, bir adı olmalı. Yığın izinde kendi dosyan görünmüyor; Node yalnız ilk on satırı gösterir ve hepsi Express'in içinde. Hangi satırı yazdığını sondaki originalPath: '*' söylüyor. Aynı değişiklik isteğe bağlı parametreleri de etkiledi: Express 4'teki /api/sampiyonlar/:id? artık Unexpected ? at index 20 hatası verir; Express 5'teki yazımı /api/sampiyonlar{/:id}.
Nasıl düzelir? 404 için yola hiç gerek yok: app.use((req, res) => { ... }), yukarıdaki gibi. Gerçekten "her şeye uyan" bir route gerekirse Express 5'te *'a ad verilir: app.get("/*yol", ...). Bu yazım kök adrese (/) uymaz; onu da kapsasın istersen süslü paranteze al: app.get("/{*yol}", ...).
Hata yakalayıcı
Bozuk bir gövde gönder (curl.exe --json "Lux" http://localhost:3000/api/sampiyonlar) ve sunucunun terminaline bak:
POST /api/sampiyonlar 400 2 msSyntaxError: Unexpected token 'L', "Lux" is not valid JSON at JSON.parse (<anonymous>) at createStrictSyntaxError (C:\kod\sampiyon-api\node_modules\body-parser\lib\types\json.js:137:10) ... at endReadableNT (node:internal/streams/readable:1757:12) { expose: true, statusCode: 400, status: 400, body: 'Lux', type: 'entity.parse.failed'}express.json() gövdeyi okuyamadı ve bir hata fırlattı. Hatanın altındaki alanlara bak: status: 400 "suç istemcide" demek. expose: true ise "bu mesajı istemciye göstermek güvenli" (expose, İngilizcede "açığa çıkarmak"). Express'in kendi ara katmanları, istemcinin bozuk isteğinden doğan hatalara bu iki alanı koyar. Fırlayan her hata normal zincirden çıkar ve bir hata yakalayıcı arar; sen yazmadıysan Express'in varsayılanı HTML sayfasını gönderir. 404 işleyicisinin altına kendininkini yaz:
app.use((hata, req, res, next) => { if (hata.expose) { return res.status(hata.status).json({ hata: "İstek okunamadı", ayrinti: hata.message }) } console.error(hata) res.status(500).json({ hata: "Sunucuda bir hata oluştu" })})- Dört parametre şart. Express, fonksiyonun kaç parametre aldığına bakarak hata yakalayıcıyı tanır.
next'i kullanmasan da silme; üç parametreyle yazılan fonksiyon sıradan bir ara katman sayılır ve hatalar onun yanından geçip yine Express'in HTML sayfasına düşer. hata.exposedoğruysa hata istemcinin isteğinden doğdu ve mesajında sunucuna dair bir sır yok: hatanın kendi kodunu (hata.status, ör.400) ve mesajını istemciye ver.- Yoksa hata senin kodundan çıktı: bir
TypeError, yanlış yazılmış bir ad. Ayrıntısınıconsole.errorile terminale yaz; istemciye yalnız genel bir cümle ve500gönder. Dosya yollarını ve kod satırlarını dışarı vermek, saldırgana sunucunun haritasını vermektir (Ders 31).
PS C:\kod\sampiyon-api> curl.exe -i --json "Lux" http://localhost:3000/api/sampiyonlarHTTP/1.1 400 Bad RequestX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 88ETag: W/"58-MrUthV+q7X5yvoqfPTkr+RzAh4Q"Date: Sun, 11 Oct 2026 18:11:52 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"İstek okunamadı","ayrinti":"Unexpected token 'L', \"Lux\" is not valid JSON"}PS C:\kod\sampiyon-api> (Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body ('{"isim": "' + ('A' * 200000) + '"}')).idInvoke-RestMethod : {"hata":"İstek okunamadı","ayrinti":"request entity too large"}...İkinci istek Ders 27'deki 200.000 harflik gövde. express.json() varsayılan olarak 100 kB'tan büyük gövdeyi okumaz ve 413 ile reddeder: kötü niyetli biri sunucunun belleğini dev bir gövdeyle dolduramasın diye. Kayıt satırı POST /api/sampiyonlar 413 1 ms yazar.
async route'larda hata
Blok 5'te route'ların içinde veritabanını await ile bekleyeceksin. async bir route'ta fırlayan hataya ne olduğunu şimdiden gör. 404 işleyicisinin üstüne geçici bir deneme route'u ekle:
app.get("/api/hata-deneme", async (req, res) => { throw new Error("Bilerek fırlatılan hata")})PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/api/hata-denemeHTTP/1.1 500 Internal Server ErrorX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 36ETag: W/"24-TsKPxtO6xJFSG+mdyLejsJ81Lpo"Date: Sun, 11 Oct 2026 18:11:52 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Sunucuda bir hata oluştu"}Error: Bilerek fırlatılan hata at file:///C:/kod/sampiyon-api/sunucu.js:92:11 at Layer.handleRequest (C:\kod\sampiyon-api\node_modules\router\lib\layer.js:152:17) ...GET /api/hata-deneme 500 1 msİstemci genel cümleyi aldı, ayrıntı terminale düştü, sunucu ayakta. Express 5, async bir fonksiyonun reddedilen Promise'ini (Ders 15) kendisi yakalayıp hata yakalayıcıya iletir. Express 4'ten farkı: aynı kodu Express 4 ile denediğimizde sunucu Error: Bilerek fırlatılan hata yazıp kapandı, curl.exe de Recv failure: Connection was reset aldı. İnternetteki eski örneklerde her async route'u saran try/catch ve next(hata) satırlarının sebebi bu; Express 5'te onlara gerek yok. Denedikten sonra deneme route'unu sil.
Statik dosyalar: express.static
API'nin yanında bir web sayfası da sunmak için Express'in hazır ara katmanı yeter. Proje klasöründe public adında bir klasör aç ve içine iki dosya koy. Sayfa Ders 19 ve 22'de öğrendiklerinden ibaret:
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Şampiyonlar</title> <style> body { font-family: system-ui, sans-serif; max-width: 420px; margin: 24px auto; padding: 0 16px; } li { padding: 6px 0; border-bottom: 1px solid #ddd; } </style></head><body> <h1>Şampiyonlar</h1> <p id="durum">Yükleniyor...</p> <ul id="liste"></ul> <script src="uygulama.js"></script></body></html>const durum = document.querySelector("#durum")const liste = document.querySelector("#liste") async function yukle() { try { const yanit = await fetch("/api/sampiyonlar") if (!yanit.ok) { throw new Error(`İstek başarısız: ${yanit.status}`) } const sampiyonlar = await yanit.json() durum.textContent = `${sampiyonlar.length} şampiyon` for (const s of sampiyonlar) { const satir = document.createElement("li") satir.textContent = `${s.isim} (${s.rol})` liste.append(satir) } } catch (hata) { durum.textContent = "Liste yüklenemedi" }} yukle()Bu dosya tarayıcıda çalışacak, Node'da değil; yapısı Ders 24'teki fetch kodunun aynısı. sunucu.js'te app.use(express.json())'ın altına tek satır ekle ve artık gereksiz olan app.get("/") route'unu sil:
app.use(express.json())app.use(express.static("public"))express.static("public") bir ara katman üretir: istenen yolu public klasöründe arar, dosya varsa türünü bulup gönderir, yoksa next() der. / istenince klasördeki index.html'i verir.
Tarayıcıda localhost:3000'i aç:

GET / 200 15 msGET /uygulama.js 200 2 msGET /api/sampiyonlar 200 3 msTek sayfa, üç istek: sayfanın kendisi, betiği ve betiğin istediği JSON. Hepsi aynı zincirden geçti; ilk ikisini express.static, üçüncüyü route'un yanıtladı. Tarayıcın sekme simgesi için /favicon.ico da isterse o istek 404 işleyicisine düşer. Sayfa ve API aynı kökenden (localhost:3000) geldiği için Ders 24'teki CORS engeli de yok. Ders 25'teki statik site gibi, bu dosyalar olduğu gibi gönderiliyor; değişen tek şey, artık onları kendi sunucunun vermesi.
PS C:\kod\sampiyon-api> curl.exe -i http://localhost:3000/HTTP/1.1 200 OKX-Powered-By: ExpressAccept-Ranges: bytesCache-Control: public, max-age=0Last-Modified: Sun, 11 Oct 2026 18:13:51 GMTETag: W/"20d-1a12c2bd226"Content-Type: text/html; charset=utf-8Content-Length: 525Date: Sun, 11 Oct 2026 18:14:39 GMTConnection: keep-aliveKeep-Alive: timeout=5 <!doctype html>...Content-Type'ı dosyanın uzantısından buldu; Last-Modified dosyanın son kaydedilme zamanı. Tarih ve uzunluklar sende farklı çıkar.
API'yi terminalden test etmek
Bir API'yi "çalışıyor" saymadan önce her uç noktasını hem doğru hem yanlış isteklerle denersin. Bunun için bir test planı yaz: istek ve beklenen durum kodu. Sonra sırayla at ve gerçek kodu beklenenle karşılaştır.
| İstek | Beklenen |
|---|---|
GET /api/sampiyonlar | 200, dizi |
GET /api/sampiyonlar?ara=a | 200, süzülmüş dizi |
GET /api/sampiyonlar/2 · /9 · /abc | 200 · 404 · 400 |
POST geçerli gövde · {} · bozuk JSON | 201 · 400 · 400 |
PATCH /api/sampiyonlar/1 geçerli · geçersiz rol | 200 · 400 |
DELETE /api/sampiyonlar/4 · aynısı bir daha | 204 · 404 |
GET /yok · PUT /api/sampiyonlar/1 | 404 · 404 |
Bir CRUD turu, Invoke-RestMethod ile:
PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/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\sampiyon-api> Invoke-RestMethod -Method Patch -Uri http://localhost:3000/api/sampiyonlar/4 -ContentType "application/json; charset=utf-8" -Body '{"rol": "Destek"}' id isim rol-- ---- --- 4 Lux Destek PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Delete -Uri http://localhost:3000/api/sampiyonlar/4 PS C:\kod\sampiyon-api> Invoke-RestMethod http://localhost:3000/api/sampiyonlar/4Invoke-RestMethod : {"hata":"4 numaralı şampiyon yok"}...Invoke-RestMethod yalnız gövdeyi gösterir; 201 ile 200'ü ayırt edemezsin. Durum kodunu görmek için iki yol:
PS C:\kod\sampiyon-api> (Invoke-WebRequest -Method Post -Uri http://localhost:3000/api/sampiyonlar -ContentType "application/json; charset=utf-8" -Body '{"isim": "Teemo", "rol": "Nişancı"}' -UseBasicParsing).StatusCode201PS C:\kod\sampiyon-api> (Invoke-WebRequest -Method Delete -Uri http://localhost:3000/api/sampiyonlar/5 -UseBasicParsing).StatusCode204PS C:\kod\sampiyon-api> curl.exe -s -o NUL -w "%{http_code}" http://localhost:3000/api/sampiyonlar/5404Invoke-WebRequest(Ders 26) bütün yanıtı verir;.StatusCodeyalnız kodu alır.4xxya da5xxgelince o da hata fırlatır.curl.exe -s -o NUL -w "%{http_code}":-silerleme yazılarını susturur,-o NULgövdeyi Windows'un "hiçbir yer" dosyasına atar,-wyalnız durum kodunu yazar. Test planındaki her satırı tek kodla denetlemenin en kısa yolu.
Geçerli JSON'u curl.exe ile göndermek için Ders 27'deki gibi bir dosya kullan. Proje klasöründe lux.json aç, içine { "isim": "Lux", "rol": "Büyücü" } yaz:
PS C:\kod\sampiyon-api> curl.exe -i --json "@lux.json" http://localhost:3000/api/sampiyonlarHTTP/1.1 201 CreatedX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 39ETag: W/"27-TkW3sOdzMBr5oo1/GjtbgYR4+m0"Date: Sun, 11 Oct 2026 18:28:44 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"id":6,"isim":"Lux","rol":"Büyücü"}id 6 oldu: 4 ve 5 silinmiş olsa da yeniden verilmedi. Sunucunun son hâli, karşılaştırmak için:
import express from "express"import { sampiyonuDogrula } from "./dogrula.js" const app = express()const port = 3000 let sampiyonlar = [ { id: 1, isim: "Ahri", rol: "Büyücü" }, { id: 2, isim: "Garen", rol: "Dövüşçü" }, { id: 3, isim: "Jinx", rol: "Nişancı" }]let sonId = 3 app.use((req, res, next) => { const baslangic = Date.now() res.on("finish", () => { const sure = Date.now() - baslangic console.log(`${req.method} ${req.url} ${res.statusCode} ${sure} ms`) }) next()})app.use(express.json())app.use(express.static("public")) app.get("/api/sampiyonlar", (req, res) => { const { ara } = req.query if (typeof ara !== "string") { return res.json(sampiyonlar) } const aranan = ara.toLowerCase() res.json(sampiyonlar.filter((s) => s.isim.toLowerCase().includes(aranan)))}) app.get("/api/sampiyonlar/yeni", (req, res) => { if (sampiyonlar.length === 0) { return res.status(404).json({ hata: "Hiç şampiyon yok" }) } res.json(sampiyonlar[sampiyonlar.length - 1])}) function sampiyonuBul(req, res, next) { const id = Number(req.params.id) if (!Number.isInteger(id)) { return res.status(400).json({ hata: "id bir tam sayı olmalı" }) } const sampiyon = sampiyonlar.find((s) => s.id === id) if (!sampiyon) { return res.status(404).json({ hata: `${id} numaralı şampiyon yok` }) } req.sampiyon = sampiyon next()} app.get("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { res.json(req.sampiyon)}) app.post("/api/sampiyonlar", (req, res) => { const hatalar = sampiyonuDogrula(req.body) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz şampiyon", ayrintilar: hatalar }) } sonId = sonId + 1 const yeni = { id: sonId, isim: req.body.isim.trim(), rol: req.body.rol } sampiyonlar.push(yeni) res.status(201).json(yeni)}) app.patch("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { const guncel = { isim: req.body?.isim ?? req.sampiyon.isim, rol: req.body?.rol ?? req.sampiyon.rol } const hatalar = sampiyonuDogrula(guncel) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz şampiyon", ayrintilar: hatalar }) } req.sampiyon.isim = guncel.isim.trim() req.sampiyon.rol = guncel.rol res.json(req.sampiyon)}) app.delete("/api/sampiyonlar/:id", sampiyonuBul, (req, res) => { sampiyonlar = sampiyonlar.filter((s) => s.id !== req.sampiyon.id) res.status(204).end()}) app.use((req, res) => { res.status(404).json({ hata: `${req.method} ${req.url} diye bir yol yok` })}) app.use((hata, req, res, next) => { if (hata.expose) { return res.status(hata.status).json({ hata: "İstek okunamadı", ayrinti: hata.message }) } console.error(hata) res.status(500).json({ hata: "Sunucuda bir hata oluştu" })}) app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Sunucu çalışıyor: http://localhost:${port}`)})105 satır: Ders 27'deki 69 satırın yaptığından çok daha fazlasını yapıyor ve satırların çoğu şampiyonlarla ilgili. Gövdeyi okumak, adresi parçalarına ayırmak, başlıkları yazmak gibi her API'de aynı olan işleri Express üstlendi.
Görevler
Görevleri aynı projede yap; sunucu npm run dev ile açık kalsın. Şampiyon route'larına dokunmayacaksın: görevlerin kaynağı dükkândaki eşyalar. Yeni route'ları 404 işleyicisinin üstüne yaz, yoksa onlara hiç sıra gelmez. Her adımdan sonra istekleri curl.exe ile at ve durum kodunu denetle.
Eşya listesi, tek eşya ve kaldırmak
Şu üç eşyayla bir esyalar dizisi kur ve üç uç nokta yaz: GET /api/esyalar hepsini, GET /api/esyalar/:id tek eşyayı döndürsün, DELETE /api/esyalar/:id eşyayı dükkândan kaldırıp gövdesiz 204 dönsün. Tam sayı olmayan id'ye 400, olmayan eşyaya 404 dönsün.
1, Uzun Kılıç, 350 altın2, Büyü Kitabı, 435 altın3, Çevik Botlar, 300 altınPS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/esyalar/3{"id":3,"isim":"Çevik Botlar","fiyat":300}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/esyalar/4{"hata":"4 numaralı eşya yok"}PS C:\kod\sampiyon-api> curl.exe -s -o NUL -w "%{http_code}" -X DELETE http://localhost:3000/api/esyalar/3204PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/esyalar/3{"hata":"3 numaralı eşya yok"}İpucu 1
Her eşya bir obje: { id: 1, isim: "Uzun Kılıç", fiyat: 350 }. Silerken diziyi filter'ın verdiği yeni diziyle değiştireceğin için dizi let ile açılsın; şampiyon dizisinin yanında ya da eşya route'larının hemen üstünde dursun.
İpucu 2
sampiyonuBul olduğu gibi işe yaramaz, çünkü sampiyonlar dizisinde arıyor. Onun ikizini yaz: esyayiBul, bulduğunu req.esya'ya koysun. Tek eşyayı getiren ve silen iki route da onu kullanır.
Çözümü göster
let 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 esyayiBul(req, res, next) { const id = Number(req.params.id) if (!Number.isInteger(id)) { return res.status(400).json({ hata: "id bir tam sayı olmalı" }) } const esya = esyalar.find((e) => e.id === id) if (!esya) { return res.status(404).json({ hata: `${id} numaralı eşya yok` }) } req.esya = esya next()} app.get("/api/esyalar", (req, res) => { res.json(esyalar)}) app.get("/api/esyalar/:id", esyayiBul, (req, res) => { res.json(req.esya)}) app.delete("/api/esyalar/:id", esyayiBul, (req, res) => { esyalar = esyalar.filter((e) => e.id !== req.esya.id) res.status(204).end()})Sınır değerleriyle sonuçlar (sunucu yeni başlamışken, sırayla):
GET /api/esyalar→200, üç eşyaGET /api/esyalar/3→200,{"id":3,"isim":"Çevik Botlar","fiyat":300}(son eşya)GET /api/esyalar/4→404,{"hata":"4 numaralı eşya yok"}(sonun bir fazlası)GET /api/esyalar/0→404,{"hata":"0 numaralı eşya yok"}GET /api/esyalar/abcveGET /api/esyalar/2.5→400,{"hata":"id bir tam sayı olmalı"}DELETE /api/esyalar/3→204; aynısı bir daha →404;DELETE /api/esyalar/abc→400- Silmeden sonra
GET /api/esyalar→[{"id":1,"isim":"Uzun Kılıç","fiyat":350},{"id":2,"isim":"Büyü Kitabı","fiyat":435}]
Dükkân vitrini
public klasörüne dukkan.html ve dukkan.js ekle. Tarayıcıda localhost:3000/dukkan.html açılınca sayfa /api/esyalar'dan eşyaları çekip Uzun Kılıç: 350 altın biçiminde listelesin, üstte kaç eşya olduğunu yazsın. Eşya hiç yoksa liste yerine Dükkân boş yazsın.
İpucu 1
Şampiyon sayfasının iki dosyası iyi bir başlangıç: adresi, metinleri ve öğe adlarını değiştir. sunucu.js'e dokunman gerekmiyor; express.static public'teki her dosyayı zaten veriyor.
İpucu 2
Boş durumu esyalar.length === 0 ile yakala, yazıyı koy ve return ile fonksiyondan çık. Denemek için esyalar dizisini geçici olarak [] yap.
Çözümü göster
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Dükkân</title></head><body> <h1>Dükkân</h1> <p id="durum">Yükleniyor...</p> <ul id="esyalar"></ul> <script src="dukkan.js"></script></body></html>const durum = document.querySelector("#durum")const liste = document.querySelector("#esyalar") async function yukle() { try { const yanit = await fetch("/api/esyalar") if (!yanit.ok) { throw new Error(`İstek başarısız: ${yanit.status}`) } const esyalar = await yanit.json() if (esyalar.length === 0) { durum.textContent = "Dükkân boş" return } durum.textContent = `${esyalar.length} eşya` for (const e of esyalar) { const satir = document.createElement("li") satir.textContent = `${e.isim}: ${e.fiyat} altın` liste.append(satir) } } catch (hata) { durum.textContent = "Eşyalar yüklenemedi" }} yukle()
Sunucunun terminalinde üç satır görünür: GET /dukkan.html 200, GET /dukkan.js 200, GET /api/esyalar 200.
Bakım modu
Bir ara katman yaz: const bakimModu = true iken GET dışındaki her istek 503 Service Unavailable ve şu gövdeyi alsın; GET istekleri eskisi gibi çalışsın. bakimModu false olunca her şey normale dönsün.
{"hata":"Bakım var, şimdilik yalnız okuma yapılabilir"}Sonra şu soruyu cevapla: bakım varken POST /yok isteği 404 mü alır, 503 mü? Neden?
İpucu 1
Ara katman req.method'a bakar. Koşul tutarsa return res.status(503).json(...), tutmazsa next().
İpucu 2
Yeri önemli: route'lardan önce çalışmalı. Kayıt tutucunun hemen altı iyi bir yer; böylece reddedilen istekler de deftere girer.
Çözümü göster
const bakimModu = true app.use((req, res, next) => { if (bakimModu && req.method !== "GET") { return res.status(503).json({ hata: "Bakım var, şimdilik yalnız okuma yapılabilir" }) } next()})const bakimModu dosyanın üstünde, ara katman kayıt tutucunun altında. Bakım açıkken:
PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/sampiyonlar/1{"id":1,"isim":"Ahri","rol":"Büyücü"}PS C:\kod\sampiyon-api> curl.exe -i --json "@lux.json" http://localhost:3000/api/sampiyonlarHTTP/1.1 503 Service UnavailableX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 59ETag: W/"3b-ri54xRF/BTrkighCBs2JJQwDxSM"Date: Sun, 11 Oct 2026 18:18:24 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Bakım var, şimdilik yalnız okuma yapılabilir"}PS C:\kod\sampiyon-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/1{"hata":"Bakım var, şimdilik yalnız okuma yapılabilir"}PS C:\kod\sampiyon-api> curl.exe -X POST http://localhost:3000/yok{"hata":"Bakım var, şimdilik yalnız okuma yapılabilir"}PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/yok{"hata":"GET /yok diye bir yol yok"}POST /yok 503 aldı: bakım ara katmanı zincirde 404 işleyicisinden çok önce duruyor ve isteği orada yanıtlıyor. GET /yok ise ondan geçti ve 404'e ulaştı. Bozuk JSON'lu bir POST da 400 değil 503 alır, çünkü express.json()'dan önce reddediliyor. bakimModu'nu false yapıp kaydedince POST yine 201 döner.
Eşya ekle ve güncelle
İki uç nokta daha yaz:
POST /api/esyalar: gövdedekiisimvefiyatile yeni bir eşya oluşturup201ile döndürsün. Kurallar:isimboş olmayan bir metin;fiyat0 ile 5000 arasında (ikisi dahil) bir tam sayı. Kurallara uymayan istek, şampiyonlardaki gibi{ hata, ayrintilar }gövdesiyle400alsın. Kimliği dizinin uzunluğundan değil, ayrı bir sayaçtan ver: Görev 1'de silmeyi eklediğin içinesyalar.length + 1kaldırılmış bir eşyanın kimliğini yeniden verebilir.PATCH /api/esyalar/:id: gövdede gelen alanları (isim,fiyatya da ikisi) değiştirip güncel eşyayı200ile döndürsün. Güncel hâl de aynı kurallara uymalı; uymuyorsa eşya hiç değişmeden400dönsün. Fiyatı0yapmak, yani eşyayı bedava yapmak da çalışmalı.
İpucu 1
dogrula.js'e esyayiDogrula(veri) adında ikinci bir fonksiyon ekle ve export et. İçe aktarırken iki adı birden yazabilirsin: import { sampiyonuDogrula, esyayiDogrula } from "./dogrula.js".
İpucu 2
Number.isInteger(veri.fiyat) hem metni ("500") hem küsuratı (12.5) reddeder; Number'a çevirmeden doğrudan sor. Bedava eşya (0) geçmeli: !veri.fiyat yazarsan 0'ı da reddedersin (Ders 12).
İpucu 3
Kimlik için sonId'nin ikizi: let sonEsyaId = 3. PATCH için şampiyonların PATCH'ini örnek al: önce guncel objesini kur, esyayiDogrula ile denetle, sonra eşyayı değiştir. Eski değeri korurken ?? kullan, || değil: || bedava eşyanın 0'ını "yok" sayar ve eski fiyatı geri koyar.
Çözümü göster
Önce fonksiyonu tek başına dene. dogrula.js'in sonuna, bir boş satır bırakıp ekle; deneme satırlarını sonra sil:
export function esyayiDogrula(veri) { if (typeof veri !== "object" || veri === null) { return ["Gövde bir JSON objesi olmalı"] } const hatalar = [] if (typeof veri.isim !== "string" || veri.isim.trim() === "") { hatalar.push("isim boş olmayan bir metin olmalı") } if (!Number.isInteger(veri.fiyat) || veri.fiyat < 0 || veri.fiyat > 5000) { hatalar.push("fiyat 0 ile 5000 arasında bir tam sayı olmalı") } return hatalar} console.log(esyayiDogrula({ isim: "Bedava Bilet", fiyat: 0 }))console.log(esyayiDogrula({ isim: "Ölüm Şapkası", fiyat: 5000 }))console.log(esyayiDogrula({ isim: "Altın Kaşık", fiyat: 5001 }))console.log(esyayiDogrula({ isim: "", fiyat: "500" }))[][][ 'fiyat 0 ile 5000 arasında bir tam sayı olmalı' ][ 'isim boş olmayan bir metin olmalı', 'fiyat 0 ile 5000 arasında bir tam sayı olmalı']Sonra sunucu.js'te eşya dizisinin altına let sonEsyaId = 3, eşya route'larının arasına da şu ikisi:
app.post("/api/esyalar", (req, res) => { const hatalar = esyayiDogrula(req.body) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz eşya", ayrintilar: hatalar }) } sonEsyaId = sonEsyaId + 1 const yeni = { id: sonEsyaId, isim: req.body.isim.trim(), fiyat: req.body.fiyat } esyalar.push(yeni) res.status(201).json(yeni)}) app.patch("/api/esyalar/:id", esyayiBul, (req, res) => { const guncel = { isim: req.body?.isim ?? req.esya.isim, fiyat: req.body?.fiyat ?? req.esya.fiyat } const hatalar = esyayiDogrula(guncel) if (hatalar.length > 0) { return res.status(400).json({ hata: "Geçersiz eşya", ayrintilar: hatalar }) } req.esya.isim = guncel.isim.trim() req.esya.fiyat = guncel.fiyat res.json(req.esya)})Sunucu yeni başlamışken sırayla gönderdiğimiz istekler:
DELETE /api/esyalar/3, sonraPOSTile{"isim": "Bedava Bilet", "fiyat": 0}→201,{"id":4,"isim":"Bedava Bilet","fiyat":0}. Kaldırılan Çevik Botlar'ın 3'ü yeniden verilmedi;esyalar.length + 1olsaydı yeni eşya 3 numarayı alırdı.POSTile{"isim": "Ölüm Şapkası", "fiyat": 5000}→201, id 5POSTile{"isim": "Altın Kaşık", "fiyat": 5001},{"isim": "Borçlu Kalkan", "fiyat": -1},{"isim": "Metin Fiyat", "fiyat": "500"}ve{"isim": "Küsuratlı", "fiyat": 12.5}→400,["fiyat 0 ile 5000 arasında bir tam sayı olmalı"]POSTile{"fiyat": 100}→400,["isim boş olmayan bir metin olmalı"]PATCH /api/esyalar/1ile{"fiyat": 0}→200,{"id":1,"isim":"Uzun Kılıç","fiyat":0}PATCH /api/esyalar/1ile{"isim": " Kısa Kılıç "}→200,{"id":1,"isim":"Kısa Kılıç","fiyat":0}: boşluklar kırpıldı, fiyat korunduPATCH /api/esyalar/1ile{"fiyat": 5001},{"fiyat": "300"}ya da{"isim": " "}→400;PATCH /api/esyalar/9→404
Sınırlar tam yerinde: 0 ve 5000 geçiyor, -1 ve 5001 geçmiyor. ?? yerine || yazsaydın {"fiyat": 0} isteği hata vermeden {"id":1,"isim":"Uzun Kılıç","fiyat":350} döndürürdü: çalışıyor görünen ama yanlış bir güncelleme.
Ucuz eşyalar ve en ucuzu
İki özellik ekle:
GET /api/esyalar?enFazla=400yalnız fiyatı 400 ya da daha az olan eşyaları döndürsün.enFazlayoksa hepsi gelsin; 0 ya da daha büyük bir tam sayı değilse400dönsün.GET /api/esyalar/en-ucuzen ucuz eşyayı döndürsün; dükkânda eşya yoksa404.
PS C:\kod\sampiyon-api> curl.exe "http://localhost:3000/api/esyalar?enFazla=300"[{"id":3,"isim":"Çevik Botlar","fiyat":300}]PS C:\kod\sampiyon-api> curl.exe http://localhost:3000/api/esyalar/en-ucuz{"id":3,"isim":"Çevik Botlar","fiyat":300}İpucu 1
req.query.enFazla metin olarak gelir. Number ile çevir, Number.isInteger ile denetle. Dikkat: Number("") sonucu 0; ?enFazla= boş geldiğinde bunu ayrıca yakala.
İpucu 2
En ucuzu bulmak Ders 5'teki "kemer" yöntemi: ilk eşyayı aday say, ötekileri tek tek onunla karşılaştır, daha ucuzsa aday o olsun.
İpucu 3
/en-ucuz 400 alıyorsa route sırasına bak: /api/esyalar/:id'nin üstünde olmalı.
Çözümü göster
app.get("/api/esyalar", (req, res) => { const { enFazla } = req.query if (enFazla === undefined) { return res.json(esyalar) } const sinir = Number(enFazla) if (enFazla === "" || !Number.isInteger(sinir) || sinir < 0) { return res.status(400).json({ hata: "enFazla 0 ya da daha büyük bir tam sayı olmalı" }) } res.json(esyalar.filter((e) => e.fiyat <= sinir))}) app.get("/api/esyalar/en-ucuz", (req, res) => { if (esyalar.length === 0) { return res.status(404).json({ hata: "Dükkânda eşya yok" }) } let enUcuz = esyalar[0] for (const e of esyalar) { if (e.fiyat < enUcuz.fiyat) { enUcuz = e } } res.json(enUcuz)})Liste route'unu Görev 1'deki yerinde değiştirdik; /en-ucuz onun altında, /api/esyalar/:id'nin üstünde. Sınır değerleriyle:
?enFazla=300→ Çevik Botlar (300 dahil);?enFazla=299→[];?enFazla=0→[]?enFazla=435→ üç eşya?enFazla=-5,?enFazla=abc,?enFazla=ve?enFazla=2.5→400,{"hata":"enFazla 0 ya da daha büyük bir tam sayı olmalı"}/api/esyalar/en-ucuz→ Çevik Botlar. Görev 4'teki Bedava Bilet eklendikten sonra ise{"id":4,"isim":"Bedava Bilet","fiyat":0}.
Hata avı: dükkân sunucusu
Aşağıdaki sunucunun dört hatası ve bir eksiği var. Proje klasöründe dukkan-sunucu.js adıyla kaydet. VS Code'un terminal panelindeki + düğmesiyle üçüncü bir terminal aç ve orada node --watch dukkan-sunucu.js ile çalıştır; istekleri her zamanki ikinci terminalden at. 3001 numaralı kapıyı kullandığı için ana sunucunla çakışmaz. Hataları gözle aramak yerine belirtilerden yola çık.
import express from "express" const app = express()const port = 3001 let altin = 500const esyalar = [ { id: 1, isim: "Uzun Kılıç", fiyat: 350 }, { id: 2, isim: "Büyü Kitabı", fiyat: 435 }] app.get("/api/altin", (req, res) => { res.json({ altin: altin })}) app.get("/api/esyalar/:id", (req, res) => { const esya = esyalar.find((e) => e.id === req.params.id) if (!esya) { return res.status(404).json({ hata: "Böyle bir eşya yok" }) } res.json(esya)}) app.post("/api/satin-al", (req, res) => { const esya = esyalar.find((e) => e.id === req.body.id) if (!esya) { return res.status(404).json({ hata: "Böyle bir eşya yok" }) } if (altin < esya.fiyat) { res.status(400).json({ hata: "Altın yetmiyor" }) } altin = altin - esya.fiyat res.json({ alinan: esya.isim, kalanAltin: altin })}) app.all("*", (req, res) => { res.status(404).json({ hata: "Böyle bir yol yok" })}) app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Dükkân çalışıyor: http://localhost:${port}`)})Belirtiler, bulunuş sırasıyla:
- Sunucu açılmıyor:
PathError [TypeError]: Missing parameter name at index 1: *. curl.exe http://localhost:3001/api/esyalar/1şunu döndürüyor:{"hata":"Böyle bir eşya yok"}.Invoke-RestMethod -Method Post -Uri http://localhost:3001/api/satin-al -ContentType "application/json; charset=utf-8" -Body '{"id": 1}'500alıyor; sunucunun terminalindeTypeError: Cannot read properties of undefined (reading 'id').- Kılıcı aldıktan sonra (150 altın kalır) kitabı almaya çalışınca istemci
{"hata":"Altın yetmiyor"}alıyor, ama terminaldeERR_HTTP_HEADERS_SENTvar ve/api/altin{"altin":-285}diyor.
Düzeltince 500 altınla kılıç alınabilmeli, kitap alınamamalı ve altın asla eksiye düşmemeli. Son olarak eksiği tamamla: dükkânın hata yakalayıcısı yok. Bozuk gövdeli bir istek (curl.exe --json "Lux" http://localhost:3001/api/satin-al) ve gövdesiz bir istek (curl.exe -X POST http://localhost:3001/api/satin-al) yığın izli HTML yerine JSON almalı.
İpucu 1
Her belirti bu derste bir hata kartına karşılık geliyor: eski yol yazımı, metin ile sayı, eksik ara katman ve unutulan return.
İpucu 2
1: app.all("*", ...) yerine yolsuz app.use. 2: req.params.id metin. 3: req.body neden undefined? 4: 400'den sonra fonksiyon devam ediyor.
İpucu 3
Hata yakalayıcıyı sunucu.js'teki gibi yaz: dört parametre, 404 işleyicisinin altında. Bozuk JSON'un hatası expose taşır ve 400 olur. Gövdesiz istekte ise req.body undefined kalır; req.body.id bir TypeError fırlatır ve o 500 olur.
Çözümü göster
Dört düzeltme ve bir ekleme: const app = express()'in altına app.use(express.json()); e.id === Number(req.params.id); return res.status(400).json({ hata: "Altın yetmiyor" }); 404 için yolsuz app.use((req, res) => { ... }); ve onun altına hata yakalayıcı. Düzeltilmiş dosya:
import express from "express" const app = express()const port = 3001 app.use(express.json()) let altin = 500const esyalar = [ { id: 1, isim: "Uzun Kılıç", fiyat: 350 }, { id: 2, isim: "Büyü Kitabı", fiyat: 435 }] app.get("/api/altin", (req, res) => { res.json({ altin: altin })}) app.get("/api/esyalar/:id", (req, res) => { const esya = esyalar.find((e) => e.id === Number(req.params.id)) if (!esya) { return res.status(404).json({ hata: "Böyle bir eşya yok" }) } res.json(esya)}) app.post("/api/satin-al", (req, res) => { const esya = esyalar.find((e) => e.id === req.body.id) if (!esya) { return res.status(404).json({ hata: "Böyle bir eşya yok" }) } if (altin < esya.fiyat) { return res.status(400).json({ hata: "Altın yetmiyor" }) } altin = altin - esya.fiyat res.json({ alinan: esya.isim, kalanAltin: altin })}) app.use((req, res) => { res.status(404).json({ hata: "Böyle bir yol yok" })}) app.use((hata, req, res, next) => { if (hata.expose) { return res.status(hata.status).json({ hata: "İstek okunamadı", ayrinti: hata.message }) } console.error(hata) res.status(500).json({ hata: "Sunucuda bir hata oluştu" })}) app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Dükkân çalışıyor: http://localhost:${port}`)})PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3001/api/satin-al -ContentType "application/json; charset=utf-8" -Body '{"id": 1}' alinan kalanAltin------ ----------Uzun Kılıç 150 PS C:\kod\sampiyon-api> Invoke-RestMethod -Method Post -Uri http://localhost:3001/api/satin-al -ContentType "application/json; charset=utf-8" -Body '{"id": 2}'Invoke-RestMethod : {"hata":"Altın yetmiyor"}...PS C:\kod\sampiyon-api> curl.exe http://localhost:3001/api/altin{"altin":150}PS C:\kod\sampiyon-api> curl.exe http://localhost:3001/api/esyalar/1{"id":1,"isim":"Uzun Kılıç","fiyat":350}PS C:\kod\sampiyon-api> curl.exe http://localhost:3001/yok{"hata":"Böyle bir yol yok"}PS C:\kod\sampiyon-api> curl.exe --json "Lux" http://localhost:3001/api/satin-al{"hata":"İstek okunamadı","ayrinti":"Unexpected token 'L', \"Lux\" is not valid JSON"}PS C:\kod\sampiyon-api> curl.exe -X POST http://localhost:3001/api/satin-al{"hata":"Sunucuda bir hata oluştu"}Sınır: altın tam yetiyorsa alım kabul edilmeli, çünkü koşul altin < esya.fiyat. Başlangıç altınını 785 yapınca (350 + 435) iki eşya da alınır ve {"altin":0} kalır. Bir tuzak daha: -Body '{"id": "1"}' 404 alır, çünkü JSON'daki tırnaklı "1" metindir. Gövdedeki JSON türleri korur; sayı bekliyorsan sayı gönderilmeli.
Son istekteki 500 aslında hâlâ bir eksik: hatalı olan istemci, sunucu değil. req.body?.id yazarsan aynı istek 404 alır; en doğrusu gövdeyi şampiyonlardaki gibi doğrulayıp 400 döndürmek.
Kendini sına
Soru 1
GET /api/sampiyonlar/7 isteğinde bu satır ne yazar?
console.log(typeof req.params.id, req.params.id === 7)Cevabı göster
string false. Adresten gelen her değer metindir: req.params.id "7", sayı 7 değil. === türlere de baktığı için karşılaştırma false olur. Önce Number(req.params.id) ile çevirmek gerekir.
Soru 2
Kayıt tutucu bu sırayla yazıldı. GET /api/sampiyonlar ve GET /yok isteklerinden hangisi terminale yazılır?
app.get("/api/sampiyonlar", (req, res) => { res.json(sampiyonlar)}) app.use((req, res, next) => { console.log(req.method, req.url) next()})Cevabı göster
Yalnız GET /yok. Express zinciri yazıldığı sırayla dener. /api/sampiyonlar isteğini yukarıdaki route yanıtladı ve next() demedi; istek kayıt tutucuya hiç ulaşmadı. /yok'a uyan route olmadığı için istek aşağı indi ve deftere yazıldı. Her isteği görmesi gereken ara katmanlar route'ların üstüne yazılır.
Soru 3
Hangi durum kodunu döndürürsün? (a) Silme başarılı. (b) PATCH'te rol "Aşçı". (c) Olmayan bir şampiyonu silmek. (d) Yeni şampiyon eklendi. (e) Route'un içinde bir TypeError fırladı.
Cevabı göster
(a) 204, gövde yok. (b) 400, istek kurallara uymuyor. (c) 404. (d) 201 ve yeni kayıt. (e) 500: hata istemcinin değil, sunucunun kodunun. Bu derste hata yakalayıcın, expose taşımayan her hatayı 500'e çeviriyor.
Soru 4
Hata yakalayıcıyı app.use((hata, req, res) => { ... }) diye üç parametreyle yazarsan bozuk bir JSON gönderildiğinde ne olur?
Cevabı göster
Express, fonksiyonun parametre sayısına bakarak hata yakalayıcıyı tanır. Üç parametreli fonksiyon sıradan bir ara katman sayılır ve hatalar onun yanından geçer. Bozuk JSON yine Express'in varsayılan yığın izli HTML sayfasıyla 400 alır. next'i kullanmasan da dördüncü parametre yerinde durmalı.
Soru 5
Doğrulamadan önceki sunucuya {} gövdesi gönderince yanıt neden {"id":5} oldu? isim ve rol nereye gitti?
Cevabı göster
Gövde boş bir objeydi; req.body.isim ve req.body.rol undefined geldi ve yeni kayda öyle yazıldı. res.json veriyi JSON.stringify ile metne çevirir ve değeri undefined olan alanları atlar. Geriye yalnız id kaldı. Hata vermeyen bu kayıt, doğrulamanın neden gerektiğinin kanıtı.
Soru 6
Mesajı oku: ne yanlış, nasıl düzelir?
PathError [TypeError]: Unexpected ? at index 20: /api/sampiyonlar/:id?; visit https://git.new/pathToRegexpError for infoCevabı göster
Yolun 20. konumunda beklenmeyen bir ? var: /api/sampiyonlar/:id?. Bu, Express 4'te "isteğe bağlı parametre" demekti; Express 5'te bu yazım kaldırıldı ve sunucu açılırken çöküyor. Express 5'te isteğe bağlı kısım süslü paranteze alınır: /api/sampiyonlar{/:id}. Çoğu zaman en temizi iki ayrı route yazmak: biri listeye, biri tek kayda.
Soru 7
public/uygulama.js sayfadan fetch("/api/sampiyonlar") ile istek atıyor, ama Ders 24'teki CORS hatası çıkmıyor. Neden?
Cevabı göster
Sayfa da API de aynı kökenden geliyor: http://localhost:3000. CORS yalnız bir sayfa başka bir kökene istek attığında devreye girer. fetch'e verilen /api/sampiyonlar adresi / ile başladığı için sayfanın geldiği sunucuya gider.
Kayıt noktası
Projeni Git'e kaydet. sampiyon-api yeni bir proje klasörü olduğu için önce git init gerekiyor; komutları ikinci terminalde yaz:
API'n şu an her yeniden başlatmada ilk üç şampiyona dönüyor; bir sonraki derste veriyi dosyaya yazıp kalıcı yapacak, port gibi ayarları ve şifreleri koddan çıkarıp ortam değişkenlerine taşıyacaksın.