Bölüm 7 / 13
Express
Rota, middleware, hata yönetimi.
13 dakikalık okuma
4. derste çerçevesiz sunucunun üç derdini yaşadın:
yönlendirme elle, gövde okuma elle, ortak işlere yer yok. Express bu üç derde
verilmiş, yirmi yıla yaklaşan minimal cevaptır — Node dünyasının en yaygın web
katmanı. Bu ders rota ve req/res sözlüğünü, middleware zincirini
(5. hattın ASP.NET boru hattıyla birebir kıyaslı), 4 parametreli hata
middleware'ini ve CORS kavramını işler. İki senet de burada ödenir: 10-07 ve
11-11'de kavramını verdiğimiz SPA fallback'in gerçek sunucu kodu ve 3. derste
mekanizmasını kurduğumuz modül düzeyi durumun istekler arası ortaklığının
canlı kanıtı.
01Ne öğreneceksin
- Rota sözlüğü:
app.get/post,req.params/query/body, durum kodlu JSON yanıt - Middleware zinciri:
use,next(), sıranın anlamı — ASP.NET Core boru hattıyla satır satır kıyas - Hata middleware'i: 4 parametreli imza, async hatalarının Express 4/5 farkı, merkezi hata sözleşmesi
express.static+ SPA fallback'in gerçek kodu — 10-07/11-11 senedi ödenir- Modül düzeyi durumun canlı kanıtı: tek süreç, ortak bellek — 11-09/12-03 zincirinin kapanışı
- CORS kavramı: tarayıcı politikası, preflight, "izin sunucudan" — 13. hat kapısı
02İlk uygulama: rota ve req/res sözlüğü
npm install express — hattın ilk gerçek bağımlılığı (3. dersin
terazisiyle: bakımı canlı, topluluğu dev, kilosu makul, çıkış maliyeti düşük).
Çerçevesiz sürümün Express hâli:
import express from "express";
const app = express();
app.use(express.json()); // JSON gövdeyi req.body'ye ayrıştır
const basvurular = [{ id: 1, unvan: "Yazılım Uzmanı", durum: "beklemede" }];
app.get("/api/basvurular", (req, res) => {
res.json(basvurular); // content-type + serileştirme otomatik
});
app.get("/api/basvurular/:id", (req, res) => { // :id → rota parametresi
const kayit = basvurular.find(b => b.id === Number(req.params.id));
if (!kayit) return res.status(404).json({ hata: "bulunamadı" });
res.json(kayit);
});
app.post("/api/basvurular", (req, res) => {
const { unvan } = req.body; // express.json() sayesinde hazır
if (!unvan) return res.status(400).json({ hata: "unvan zorunlu" });
const yeni = { id: basvurular.length + 1, unvan, durum: "beklemede" };
basvurular.push(yeni);
res.status(201).json(yeni); // 201 + gövde — 6. hat sözleşmesi
});
app.listen(3000, () => console.log("http://localhost:3000"));
4. dersin üç derdinden ikisi tek ekranda kapandı: rota tanımı bildirimseldir
(:id parametresi req.params'a düşer; sorgu dizgesi
req.query'de — ikisi de dizgedir, sayı gerekiyorsa çevir ve
doğrula: 11-12'nin coerce dersindeki aynı sınır ilkesi), gövdeyi
express.json() okur (boyut sınırı da koyar — varsayılan 100 KB; 2.
dersin dev-JSON bloğuna karşı ilk savunma). res.json serileştirme +
header işini, res.status durum kodunu üstlenir; 6. hattın REST
sözleşmesi (404/400/201 kararları) aynen buradadır. Üçüncü dert — ortak işler —
sonraki bölümün konusu. Bir üretim notu: gerçek API'de elle if (!unvan)
satırları büyümez; doğrulama 11-12'de tanıştığın zod gibi şema araçlarına devredilir
— 9. ders (nd-api) bu iskeleti katmanlara ayıracak.
03Middleware zinciri: 5. hattın tanıdık borusu
Express'in kalbi rotalar değil zincirdir: her istek, kayıt sırasıyla bir
fonksiyon dizisinden geçer; her fonksiyon ya isteği cevaplar (kısa devre) ya da
next() ile sıradakine devreder. 5. hattın middleware dersini birebir
biliyorsun — kıyas tablosu:
ASP.NET Core Express
Kayıt app.Use(...) app.use(fn)
Devir await next() next()
Kısa devre next'i çağırmamak next'i çağırmamak / res göndermek
Sıra kayıt sırası = çalıştırma aynı — sıra her şeydir
Yol filtresi app.Map("/api", ...) app.use("/api", fn)
Hata yakalama exception middleware (başta) HATA middleware'i (SONDA!)
app.use((req, res, next) => { // 1) log — herkesten önce
const baslangic = Date.now();
res.on("finish", () => // yanıt bitince (res bir stream!)
console.log(req.method, req.url, res.statusCode, Date.now() - baslangic, "ms"));
next();
});
app.use(express.json()); // 2) gövde ayrıştırma
app.use("/api/yonetim", yetkiKontrol); // 3) yalnız bu yola özel
// ... rotalar ...
app.use((req, res) => res.status(404).json({ hata: "yol yok" })); // 4) hiçbiri tutmadı
İki farkın altını çiz. (1) ASP.NET'te next await'lenir —
middleware, sonrakilerin bitişini bekleyip devamında iş yapabilir (sarmalama
modeli); Express'te next() dönüş değersizdir — "yanıttan sonra iş"
için res.on("finish") gibi olaylara abone olursun (4. dersin
EventEmitter'ı iş başında). (2) Hata middleware'inin yeri terstir: ASP.NET'te
istisna yakalayıcı boru hattının başına konur (sarmalayıp beklediği için alt
katmanların hatasını görür); Express'te sarmalama olmadığından hata yakalayıcı
zincirin sonuna konur — hatalar ona next(err) ile iletilir.
Bu, bir sonraki bölümün kapısı. Sıra hatası klasiği her iki dünyada aynıdır:
express.json()'ı rotalardan sonra kaydeden kod, req.body'yi
hep undefined görür — zincirde aşağıda kalan, yukarıyı etkileyemez.
04Hata yönetimi: 4 parametreli imza
Express hata yakalayıcıyı imzadan tanır: tam 4 parametreli fonksiyon
(err, req, res, next) hata middleware'idir ve zincirde ancak bir hata
oluştuğunda ya da next(err) çağrıldığında devreye girer:
class ApiHatasi extends Error {
constructor(durum, mesaj) { super(mesaj); this.durum = durum; }
}
app.get("/api/basvurular/:id", async (req, res, next) => {
try {
const kayit = await depo.bul(Number(req.params.id));
if (!kayit) throw new ApiHatasi(404, "başvuru bulunamadı");
res.json(kayit);
} catch (err) { next(err); } // hatayı zincirin sonuna İLET
});
app.use((err, req, res, next) => { // 4 parametre = hata middleware'i
const durum = err instanceof ApiHatasi ? err.durum : 500;
if (durum === 500) console.error(err); // beklenmeyeni logla (iz sürme için)
res.status(durum).json({ hata: durum === 500 ? "sunucu hatası" : err.message });
});
Üç kural bu deseni üretime taşır. (1) Async tuzağı: Express 4, senkron
throw'u kendisi yakalar ama async handler'ın reddini yakalamaz —
try/catch + next(err) yazmazsan hata, 6. dersteki gibi sahipsiz
rejection olur ve süreci düşürür. Express 5 bu açığı kapattı (async red otomatik
next(err)'e gider); sahada iki sürüm de yaşadığından iki davranışı da
bil — eski projede her async handler'ı saran yardımcı fonksiyonlar
(asyncHandler deseni) görürsün, sebebi budur. (2) Tek sözleşme:
hata gövdesi tüm API'de aynı şekildedir — 6. hattın ProblemDetails fikrinin Express
hâli; 500'de iç detay sızdırılmaz (stack trace istemciye gitmez — loglanır).
(3) Sınıflandır: beklenen hatalar (ApiHatasi) durum koduyla
taşınır; beklenmeyenler 500'dür ve 6. dersin felsefesi devrededir — loglanır,
istek düzeyinde cevaplanır, süreç yalnız gerçekten tanımsız durumda çöker.
05Statik servis ve SPA fallback: senet ödemesi
10-07'de Angular router'ı, 11-11'de React Router'ı anlatırken aynı cümleyi
kurduk: "Kullanıcı /basvurular/42'yi yenilediğinde bu isteği
sunucu karşılar; SPA'nın çalışması için sunucunun bilinmeyen yollara
index.html döndürmesi gerekir — kodunu 12. hatta yazacağız."
Ödeme günü:
import path from "node:path";
import { fileURLToPath } from "node:url";
const kok = path.dirname(fileURLToPath(import.meta.url));
// 1) API rotaları ÖNCE — /api/* asla index.html'e düşmemeli
app.use("/api", apiRotalari);
// 2) Derlenmiş SPA dosyaları: js/css/görsel — varsa dosyayı ver
app.use(express.static(path.join(kok, "dist")));
// 3) SPA FALLBACK: hiçbir dosyaya uymayan GET → index.html
app.get("*", (req, res) => {
res.sendFile(path.join(kok, "dist", "index.html"));
});
Sıra, bölüm 03'ün dersinin ta kendisidir: API → statik → fallback.
Fallback öne alınsaydı her istek — main.js dahil — HTML alırdı;
statikten önce API olmasaydı /api/bilinmeyen 404 yerine
index.html döndürür, frontend'e "JSON bekliyordum, HTML geldi" diye
yansırdı (11-10'da gördüğün o meşhur Unexpected token '<' hatasının
sunucu tarafı sebebi). Mekanizmayı da adlandır: express.static, istenen
yol dist içinde gerçek bir dosyaya denk geliyorsa onu döndürür
(dosya yolları 4. dersin path disipliniyle kurulur; static, traversal'a
karşı kök dışına çıkışı kendisi engeller), değilse next() der ve top
fallback'e düşer — kullanıcı hangi derin bağlantıyı yenilerse yenilesin
index.html yüklenir, URL'yi oradan router devralır (10-07/11-11'in
istemci tarafı). Üretimde bu işi çoğu zaman nginx üstlenir (17. hat) ama desen
aynıdır; Node tarafında yazabilen, nginx config'ini de okuyabilir.
İkinci senet küçük ama ders yüklü. Bölüm 02'deki basvurular dizisi
modül düzeyindedir — 3. dersin mekanizması ve 11-09'un uyarısı gereği bu
dizi sürecin, yani bütün kullanıcıların ortak belleğidir: iki farklı
tarayıcıdan POST atarsan ikisi de aynı listede birikir; süreç yeniden başlayınca
hepsi uçar. Alıştırmada bunu gözünle göreceksin. Ders örneği için kusur değil —
gerçek uygulamada bu durumun evi veritabanıdır (9. ders) ve "istek başına veri
modüle yazılmaz" kuralı artık üç kez kanıtlanmış bir refleks olmalı.
06CORS: tarayıcının sorusu, sunucunun cevabı
Geliştirme sırasında frontend localhost:5173, API
localhost:3000 — ve tarayıcı konsolunda kırmızı satır: "blocked by
CORS policy". Mekanizmayı doğru yerleştir: bu sunucunun değil tarayıcının
politikasıdır (same-origin: betik, kendi origin'i dışına yaptığı isteğin yanıtını
okuyamaz — origin = şema + alan adı + port). CORS, sunucunun tarayıcıya "şu
origin'lere izin veriyorum" demesinin standart yoludur: yanıtlara
access-control-allow-origin gibi header'lar eklenir; "basit"
sayılmayan istekler (ör. content-type: application/json ile POST,
ya da authorization header'ı) önce preflight tetikler —
tarayıcı OPTIONS ile sorar, sunucu izin header'larıyla cevaplarsa
gerçek istek gider. Express'te pratik çözüm cors middleware'idir:
app.use(cors({ origin: "https://uygulama.ornek.gov.tr" })) — origin
listesi bilinçli ve dar tutulur; her yere açmak (*) yalnız
herkese açık, kimliksiz API'lerde meşrudur. İki cümlelik sınır taşı: CORS bir
güvenlik duvarı değildir — curl ve sunucular politikayı hiç uygulamaz;
yetkilendirme her zaman API'nin kendi işidir (11-11'in "koruma UX, yetki API'de"
cümlesinin sunucu ucu). Kimlikli isteklerin (cookie, token) CORS'la kesişimi ise
13. hattın işidir — kapı burada açıldı, içerisi orada.
07Mini alıştırma — API + SPA aynı süreçte
Yaklaşık 20 dakika · npm init -y + npm install express cors
- Bölüm 02'deki API'yi kur;
curlile dört senaryoyu dene: liste, tekil kayıt, olmayan id (404), gövdesiz POST (400). Log middleware'ini ekle — her satırda metot, yol, durum, süre görün. - Hata zinciri:
/api/patlarotasında async bir fonksiyon içindethrowyap. Öncetry/catch + next(err)olmadan dene ve sürecin kaderini gözle (6. dersin floating rejection'ı); sonra desenle sarıp merkezi hata middleware'inden tutarlı JSON döndüğünü gör. - Senet:
dist/klasörüne el yapımı birindex.html+app.jskoy (js dosyasılocation.pathname'i sayfaya yazsın). Statik + fallback zincirini kur; tarayıcıda/basvurular/42adresini doğrudan aç — index.html'in geldiğini ve betiğin yolu okuduğunu gör. Sıra bozma deneyi: fallback'i statikten öne al, neyin kırıldığını not et, geri düzelt. - Sızıntı kanıtı: normal + gizli pencereden (iki ayrı "kullanıcı") POST ile kayıt ekle; iki pencerede de listenin ortak büyüdüğünü gör. Süreci yeniden başlat — listenin sıfırlandığını gör. İki gözlemi 11-09 ve 12-03'e bağlayan ikişer cümle yaz.
- CORS:
python3 -m http.server 5173(ya da herhangi ikinci port) üzerinden servis ettiğin bir sayfadan API'yefetchat; konsoldaki CORS hatasını oku.cors({ origin: "http://localhost:5173" })ekleyip düzelt; DevTools Network'te preflightOPTIONSisteğini bul.
08Kendini sına
Middleware zincirini ASP.NET Core boru hattıyla kıyasla: üç ortak ilke, iki yapısal fark; "express.json rotalardan sonra" hatasının belirtisini söyle.
Ortak ilkeler: (1) istek, kayıt sırasıyla bir fonksiyon
dizisinden geçer — sıra her şeydir; (2) her halka ya devreder (ASP.NET'te
await next(), Express'te next()) ya da yanıtı üretip
kısa devre yapar; (3) yol filtresi ikisinde de vardır
(app.Map ↔ app.use("/api", fn)) — ortak işler (log,
kimlik, gövde ayrıştırma) rotalara kopyalanmak yerine zincire bir kez konur. İki
yapısal fark: (1) ASP.NET sarmalama modelidir — next await'lenir,
middleware alt katmanlar bittikten sonra da iş yapabilir; Express'te
next() beklenmez, "yanıt sonrası" iş için res'in
finish olayına abone olunur. (2) Bu yüzden hata yakalayıcının yeri
terstir: ASP.NET'te istisna middleware'i boru hattının başındadır (sardığı her
şeyin hatasını görür); Express'te hata middleware'i zincirin sonundadır ve hatalar
ona next(err) ile iletilir. Sıra hatasının belirtisi:
express.json() rotalardan sonra kayıtlıysa hiçbir handler'da
req.body dolu olmaz — hep undefined; zincirde aşağıda
kalan halka, yukarıdaki handler'a hizmet edemez.
Express hata middleware'ini imzasıyla anlat; async handler'ların Express 4/5 farkını ve merkezi hata sözleşmesinin üç kuralını söyle.
Express hata yakalayıcıyı imzadan tanır: tam dört parametreli
(err, req, res, next) fonksiyonu hata middleware'idir; normal akışta
atlanır, yalnız senkron bir throw yakalandığında ya da bir halka
next(err) çağırdığında devreye girer — bu yüzden zincirin sonuna
yazılır. Async farkı kritik: Express 4 async handler'ın reddettiği promise'i
görmez — try/catch ile sarıp next(err) demezsen hata sahipsiz
rejection olur ve modern Node'da süreci düşürür; sahadaki
asyncHandler sarmalayıcı deseninin varlık sebebi budur. Express 5
açığı kapattı: async reddi otomatik olarak hata middleware'ine iletir — ama iki
sürüm de sahada yaşadığından ikisini de bilmek gerekir. Merkezi sözleşmenin üç
kuralı: (1) hata gövdesi tüm API'de tek biçimdir (ProblemDetails fikrinin Express
hâli) — istemci her hatada aynı şekli ayrıştırır; (2) beklenen hatalar durum
koduyla sınıflandırılır (özel hata sınıfı), beklenmeyen her şey 500'dür;
(3) 500'de iç detay sızdırılmaz — stack trace loglanır, istemciye genel mesaj
gider; hata istek düzeyinde cevaplanır, süreç yalnız tanımsız durumda çöker.
SPA fallback senedini öde: üç katmanın (API → statik → fallback) her birinin işi nedir, sıra neden tam böyledir ve mekanizma kullanıcı deneyimine nasıl yansır?
Sorunun kökü 10-07/11-11'den: SPA'da rotalar istemcidedir;
kullanıcı /basvurular/42'yi yenilediğinde istek sunucuya gider ve
sunucuda böyle bir dosya yoktur — önlem yoksa 404. Üç katman: (1) API
rotaları en önde — /api/* istekleri JSON dünyasında kalmalı;
API'den sonra gelseydi bilinmeyen bir API yolu index.html alır, frontend'e
"JSON bekledim, HTML geldi" (Unexpected token '<') diye yansırdı.
(2) express.static — istenen yol dist içinde gerçek bir
dosyaya denk geliyorsa (js, css, görsel) onu döndürür; değilse next() ile
devreder. (3) Fallback — kalan her GET'e index.html; tarayıcı
HTML'i ve betikleri yükler, router URL'yi okuyup doğru ekranı kurar. Sıra
bozulursa: fallback öne geçse her istek HTML alır, uygulama hiç açılmaz; statik
API'nin önüne geçmesi zararsızdır ama API fallback'in arkasına düşerse API ölür.
Kullanıcıya yansıması: derin bağlantı paylaşılabilir, yenileme çalışır, 404
görünmez — SPA "gerçek site" gibi davranır. Üretimde aynı deseni çoğu zaman
nginx uygular (try_files → index.html); desen aynı olduğundan Node'da yazan,
nginx config'ini de okur.
Bölüm 02'deki basvurular dizisinin iki kaderini (ortaklık ve uçuculuk) mekanizmayla açıkla; ardından CORS'u yerine oturt: kimin politikası, preflight nedir, neden güvenlik önlemi sayılmaz?
Dizi modül düzeyindedir; 12-03'ün mekanizmasıyla modül süreç başına bir kez çalışır ve durumu süreç ömrünce yaşar. Birinci kader — ortaklık: sunucu tek süreç olduğundan o dizi bütün kullanıcıların ortak belleğidir; iki ayrı tarayıcının POST'ları aynı listede birikir — kullanıcıya özel veri modüle yazılsaydı bu bir güvenlik hatası olurdu (11-09'un SSR uyarısının kanıtlanmış hâli). İkinci kader — uçuculuk: süreç yeniden başlayınca modül yeniden çalışır, liste sıfırlanır; kalıcılık isteyen durumun evi veritabanıdır. Kural: modül düzeyi yalnız gerçekten küresel ve yeniden kurulabilir olan için (havuz, config); istek verisi istekle taşınır, kalıcı veri depoya yazılır. CORS: same-origin, tarayıcının politikasıdır — betik kendi origin'i (şema + alan + port) dışına yaptığı isteğin yanıtını okuyamaz. CORS, sunucunun izin bildirme standardıdır: yanıt header'ları izinli origin'i söyler; basit sayılmayan istekler (JSON content-type'lı POST, authorization header'ı) önce preflight tetikler — tarayıcı OPTIONS ile sorar, izin gelirse gerçek isteği yollar. Güvenlik önlemi sayılmaz çünkü politikayı yalnız tarayıcılar uygular — curl ve sunucular hiç sormaz; yetkilendirme daima API'nin kendi işidir, kimlikli isteklerin ayrıntısı 13. hattadır.
09Sınav köşesi
Yazılıda nasıl sorulur
(1) Express'te rota parametresi ile sorgu dizgesine nasıl erişilir; tip garantileri nedir? (2) express.json() middleware'i ne yapar; kayıt sırası neden önemlidir? (3) Middleware zincirinde next() çağrılmazsa ne olur? (4) ASP.NET Core boru hattı ile Express zincirinin bir benzerliğini, bir farkını yazın. (5) Hata middleware'i hangi imzayla tanınır, zincirin neresine yazılır? (6) Express 4'te async handler hatası neden merkezi yakalayıcıya düşmez; çözüm nedir? (7) SPA fallback nedir; API → statik → fallback sırasını gerekçeleyin. (8) Modül düzeyinde tutulan bir dizinin web sunucusundaki iki sonucu nedir? (9) Same-origin politikası neyi engeller; preflight isteği ne zaman tetiklenir? (10) "CORS bir güvenlik önlemi değildir" ifadesini açıklayın.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Konum — "Express, çekirdek http'nin üç derdine (routing, gövde, ortak işler) verilmiş minimal cevaptır; rota bildirimsel, gövde express.json'la, ortak işler middleware'de." (2) Zincir — "ASP.NET boru hattının birebir dengi: sıra her şeydir, next devreder, kısa devre serbest; fark sarmalamada — Express next'i beklemez, bu yüzden hata yakalayıcı başta değil sondadır ve 4 parametreli imzayla tanınır." (3) Async — "Express 4 async reddi görmez: try/catch + next(err) disiplini; Express 5 otomatik iletir — iki davranışı da bilirim." (4) Senetler — "SPA fallback'i sunucuda ben yazarım: API → statik → fallback; sırayı bozan, ya uygulamayı ya API'yi öldürür. Modül düzeyi dizinin iki kaderini kanıtladım: tüm kullanıcılara ortak, süreçle birlikte uçucu — kalıcı durum veritabanına." (5) Kıdem vuruşu — "CORS'u yerine koyarım: tarayıcının politikası, sunucunun izni; origin listesini dar tutarım ve güvenliği asla CORS'a emanet etmem — yetki her zaman API'de."
