Skip to content

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

  1. Bölüm 02'deki API'yi kur; curl ile 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.
  2. Hata zinciri: /api/patla rotasında async bir fonksiyon içinde throw yap. Önce try/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.
  3. Senet: dist/ klasörüne el yapımı bir index.html + app.js koy (js dosyası location.pathname'i sayfaya yazsın). Statik + fallback zincirini kur; tarayıcıda /basvurular/42 adresini 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.
  4. 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.
  5. CORS: python3 -m http.server 5173 (ya da herhangi ikinci port) üzerinden servis ettiğin bir sayfadan API'ye fetch at; konsoldaki CORS hatasını oku. cors({ origin: "http://localhost:5173" }) ekleyip düzelt; DevTools Network'te preflight OPTIONS isteğ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."