Mert Özen Her Satırda Daha İleri
Backend Node.js Temelleri — #11

Express 5'e Giriş: Routing, Middleware Zinciri ve Express 4'ten Değişenler

Mert Özen 14 Kas 2026 14 dk 13 görüntülenme
Express 5'e Giriş: Routing, Middleware Zinciri ve Express 4'ten Değişenler

Express 5, async handler'lardaki hataları kendiliğinden hata middleware'ine taşır, path syntax'ını path-to-regexp 8'e geçirir ve parse edilmemiş body için req.body'yi undefined bırakır. Gerçek çıktılarla Express 4'ün aynı hatada çökmesi, artık açılışta hata veren eski path'ler, üç parametreli hata handler tuzağı, Bookstore API'sinin Express'e taşınması, EADDRINUSE'u sessizce yutan listen ve NODE_ENV=production'ın JSON API'de hızı değiştirmemesi.

Sürüm notu: Bu yazıdaki örnekler Node.js 26.13.1 (LTS), Express 5.3.0 ve TypeScript 7.0.2 ile çalıştırılmıştır; karşılaştırmada Express 4.22.3 kullanılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'sini bu yazıda Express'e taşıyoruz. Taşımadan önce küçük bir deneme: aşağıdaki uygulamada route, JSON parser'dan önce kayıtlı. Aynı kodu bir kez Express 5 ile, bir kez Express 4 ile çalıştırıp JSON bir body gönderiyoruz:

import express from "express";

const app = express();

// Route önce kayıtlı, JSON parser ondan sonra
app.post("/api/orders", async (req, res) => {
    const { customerId } = req.body;
    res.status(201).json({ customerId });
});
app.use(express.json());

app.listen(3101, "127.0.0.1", () => console.log("express 5 on 3101"));
curl -sS -w "\nstatus %{http_code}\n" -X POST http://127.0.0.1:3101/api/orders -H "content-type: application/json" -d '{"customerId":"ada"}'

İki sürümde client ne görür? Sunucu ayakta kalır mı?

== Express 5
<!DOCTYPE html>
<html lang="en">
...
<pre>TypeError: Cannot destructure property &#39;customerId&#39; of &#39;req.body&#39; as it is undefined.<br> &nbsp; &nbsp;at file:///home/user/bookstore-api/labs/11/guess.ts:7:13<br> ...</pre>
...
status 500
next request: 500
== Express 4
curl: (52) Empty reply from server
status 000
curl: (7) Failed to connect to 127.0.0.1 port 3102 after 0 ms: Couldn't connect to server

İkisinde de req.body undefined, çünkü parser route'tan sonra kayıtlı ve request ona hiç ulaşmıyor. Fark hatanın akıbetinde. Express 5, async handler'ın reject olan promise'ini yakalayıp hata middleware'ine verdi; client 500 aldı, sunucu ayakta kaldı. Ama cevap, stack trace'i ve dosya yollarını içeren bir HTML sayfası. Express 4 ise promise'e bakmıyor; rejection unhandled kaldı ve 10'daki çıplak sunucuda olduğu gibi process kapandı. Bu yazıda Express 5'in routing'ini, middleware zincirini, Express 4'ten değişenleri ve HTML'deki o stack trace'i neyin belirlediğini görüyoruz.

app, Router ve Route Parametreleri

Express'te uygulama express() ile oluşturulur. Route'lar method adlarıyla eklenir (app.get, app.post), ilgili route'lar bir Router'da toplanıp bir path'e bağlanır: app.use("/api/books", booksRouter). Router'ın içindeki path'ler bu mount path'e göre yazılır; ASP.NET Core'daki MapGroup'a benzer. Path parametreleri req.params'tan, query string req.query'den okunur.

Express 5 path'leri router 2'nin kullandığı path-to-regexp 8 ile eşleştiriyor ve syntax Express 4'ten farklı:

import express from "express";

const app = express();

// Express 5 / path-to-regexp 8: wildcard'ın adı olmalı, isteğe bağlı kısımlar süslü parantezle yazılır
app.get("/covers/*file", (req, res) => res.json({ route: "/covers/*file", params: req.params }));
app.get("/api/books/:isbn{.:format}", (req, res) => res.json({ route: "/api/books/:isbn{.:format}", params: req.params }));
app.get("/api/authors{/:id}", (req, res) => res.json({ route: "/api/authors{/:id}", params: req.params }));

// Artık parse edilemeyen Express 4 syntax'ı
for (const old of ["/covers/*", "/api/books/:isbn?", "/api/books/:isbn(\\d+)"]) {
    try {
        app.get(old, (req, res) => res.end());
        console.log(`${old.padEnd(24)} accepted`);
    } catch (err) {
        console.log(`${old.padEnd(24)} ${(err as Error).message.split(";")[0]}`);
    }
}

const server = app.listen(0, "127.0.0.1", async () => {
    const { port } = server.address() as { port: number };
    for (const path of ["/covers/2026/9780441172719.jpg", "/api/books/9780441172719.json", "/api/books/9780441172719", "/api/authors", "/api/authors/42"]) {
        const body = await (await fetch(`http://127.0.0.1:${port}${path}`)).json();
        console.log(`${path.padEnd(32)} -> ${JSON.stringify(body.params)}`);
    }
    server.close();
});
/covers/*                Missing parameter name at index 9: /covers/*
/api/books/:isbn?        Unexpected ? at index 16: /api/books/:isbn?
/api/books/:isbn(\d+)    Unexpected ( at index 16: /api/books/:isbn(\d+)
/covers/2026/9780441172719.jpg   -> {"file":["2026","9780441172719.jpg"]}
/api/books/9780441172719.json    -> {"isbn":"9780441172719","format":"json"}
/api/books/9780441172719         -> {"isbn":"9780441172719"}
/api/authors                     -> {}
/api/authors/42                  -> {"id":"42"}

Üç eski syntax da route eklenirken hata fırlatıyor; uygulama hiç açılmıyor, bu iyi, çünkü sorun ilk çalıştırmada görünür. Adsız * yerine *file, ? yerine {...} yazılıyor, parametre içinde RegEx ise artık desteklenmiyor. ASP.NET Core'daki {isbn:regex(...)} gibi route constraint'lerin karşılığı yok; doğrulama handler'da ya da şemada yapılır (12). Wildcard parametresi artık bir array: her path segment'i ayrı bir element. Eşleşmeyen isteğe bağlı parametre de req.params'ta hiç yer almıyor.

Middleware Zinciri ve next()

Express'te her şey middleware: (req, res, next) imzalı bir fonksiyon. Ya cevabı yazar ya da next() ile sıradakine geçer; ikisini de yapmazsa request asılı kalır. Sıra, kayıt sırasıdır. Hata middleware'i ise dört parametrelidir: (err, req, res, next). Express ikisini fonksiyonun parametre sayısından ayırt ediyor ve bunun bir tuzağı var:

import express, { type NextFunction, type Request, type Response } from "express";

const app = express();

app.use((req, res, next) => {
    console.log("1. global middleware");
    next();
});

const books = express.Router();
books.use((req, res, next) => {
    console.log("2. router middleware");
    next();
});
books.get("/:isbn", async (req, res) => {
    console.log("3. handler, about to throw");
    throw new Error(`stock service down for ${req.params.isbn}`);
});
app.use("/api/books", books);

// Hata handler'ına benziyor ama üç parametresi var: Express onu normal middleware sayar
app.use((err: any, req: any, res: any) => {
    console.log("never called");
    res.status(500).json({ error: "from the 3-parameter handler" });
});

// Dört parametre: hatayı bu alır
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.log(`4. error middleware: ${err.message}`);
    res.status(503).json({ error: err.message });
});

const server = app.listen(0, "127.0.0.1", async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://127.0.0.1:${port}/api/books/9780441172719`);
    console.log(`client got ${res.status} ${await res.text()}`);
    server.close();
});
1. global middleware
2. router middleware
3. handler, about to throw
4. error middleware: stock service down for 9780441172719
client got 503 {"error":"stock service down for 9780441172719"}

Üç parametreli fonksiyon hiç çağrılmadı. Hata oluşunca Express aradaki normal middleware'leri atlayıp sıradaki dört parametreli middleware'e geçiyor. ESLint gibi bir araç "kullanılmayan parametre" diye next'i silmeni önerirse hata handler'ın sessizce normal bir middleware'e dönüşür. Model, ASP.NET Core Web API serisinin "Middleware Pipeline" yazısında anlattığımız pipeline'a çok benziyor; fark, Express'te cevap dönerken middleware'lerin tekrar çalışmaması. ASP.NET Core'da await next()'ten sonraki kod response yolunda çalışır; Express'te bunun karşılığı res.on("finish") gibi event'ler.

11-express-middleware-tr.png

express.json() ve req.body

express.json(), 10'da elle yazdığımız readJsonBody'nin karşılığı. Content-Type JSON olan istekleri okur, boyutu sınırlar (varsayılan 100 KB; biz 64 KB veriyoruz) ve sonucu req.body'ye koyar. İki davranışı bilmek gerekiyor. Birincisi, Express 5'te body parse edilmemişse req.body undefined; Express 4'te parser kayıtlıyken body gelmediğinde boş bir object'ti. İkincisi, Content-Type'ı JSON olmayan bir istekte parser hiçbir şey yapmaz, hata da vermez. 10'daki 415 cevabını korumak için handler'da req.is("application/json")'u kontrol ediyoruz. Parser'ın kendi hataları status alanı taşıyan error object'leri: bozuk JSON 400, büyük body 413. Express 5'in parser'ları Brotli ile sıkıştırılmış body'leri de açabiliyor.

Bookstore API'sini Express'e Taşımak

Express'i API'ye ekledik (npm install express@^5.3.0, tipler için -D @types/express); production bağımlılık ağacı bununla 66 pakete çıktı. Uygulamayı kuran kodu listen'dan ayırıp src/app.ts'e koyuyoruz; 16'da testler app'i port açmadan kullanacak:

import express from "express";
import { requestContext } from "#middleware/requestContext";
import { notFound, errorHandler } from "#middleware/errorHandler";
import { health } from "#routes/health";
import { booksRouter } from "#routes/books";
import { ordersRouter } from "#routes/orders";
import { reportsRouter } from "#routes/reports";
import { diagnosticsRouter } from "#routes/diagnostics";
import { coversRoot } from "#storage/coverStorage";

// App'i dinlemeden kurar; testler onu port açmadan kullanabilir (16. yazı)
export function createApp(): express.Express {
    const app = express();

    // Global middleware'ler her request'te bu sırayla çalışır
    app.use(requestContext);
    app.use(express.json({ limit: "64kb" }));

    // 08'de kaydedilen kitap kapakları; ETag ve Last-Modified hazır gelir, dotfile'lar yok sayılır
    app.use("/covers", express.static(coversRoot, { maxAge: "1d", index: false }));

    app.get("/health", health);
    app.use("/api/books", booksRouter);
    app.use("/api/orders", ordersRouter);
    app.use("/api/reports", reportsRouter);
    app.use("/diagnostics", diagnosticsRouter);

    // Hiçbir şey eşleşmedi: 404; sonra hata handler'ı, her zaman en sonda
    app.use(notFound);
    app.use(errorHandler);
    return app;
}

10'daki parçaların hepsi yerini buldu. Request id, abort signal ve AsyncLocalStorage bağlamı ilk middleware oldu; runWithRequestContext'e next'i veriyoruz, böylece zincirin geri kalanı bağlamın içinde çalışıyor. Merkezi try/catch ise hata middleware'ine dönüştü:

// Dört parametre: Express bir hata handler'ını böyle tanır
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
    if (currentRequestContext()?.signal.aborted) {
        log("warn", "client went away", { method: req.method, path: req.path });
        return;
    }
    if (res.headersSent) {
        // Status için çok geç; Express'in varsayılan handler'ı bağlantıyı kapatır
        log("error", "error after the response started", { error: (err as Error).message });
        return next(err);
    }
    // Bizim kodumuzdan HttpError ya da 4xx status taşıyan body-parser hataları (400, 413, 415)
    const status: number = err instanceof HttpError ? err.status : Number.isInteger(err?.status) && err.status >= 400 && err.status < 500 ? err.status : 500;
    if (status === 500) {
        log("error", "unhandled error", { error: (err as Error).stack });
    }
    if (status === 413) {
        // Body'nin geri kalanı hâlâ yolda: hepsini okumak yerine bağlantıyı kapat
        res.setHeader("connection", "close");
    }
    res.status(status).json({ error: status === 500 ? "internal server error" : (err as Error).message });
};

Route'lar router'lara taşındı. Handler'lar artık hata fırlatabiliyor ya da reject olabiliyor; Express ikisini de bu middleware'e getiriyor. Sipariş router'ından bir parça:

// POST /api/orders
ordersRouter.post("/", (req, res) => {
    if (!req.is("application/json")) {
        throw new HttpError(415, "content-type must be application/json");
    }
    const { customerId, lines } = parsePlaceOrder(req.body);
    try {
        const order = orderService.placeOrder(customerId, lines);
        res.status(201).location(`/api/orders/${order.id}`).json(order);
    } catch (err) {
        if (err instanceof UnknownBookError) {
            throw new HttpError(422, err.message);
        }
        throw err;
    }
});

10'daki curl denemelerini aynen tekrarladım:

HTTP/1.1 201 Created
X-Powered-By: Express
x-request-id: cd2b7bb8-dbe3-4e2d-a848-8a82ca018472
Location: /api/orders/ORD-1001
Content-Type: application/json; charset=utf-8
Content-Length: 163
ETag: W/"a3-8ARWLN5dbSusbI45k93fz7OK9js"
Date: Sat, 14 Nov 2026 19:03:18 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":"ORD-1001","customerId":"ada","status":"Pending","items":[{"isbn":"9780441172719","quantity":2,"unitPrice":349.9}],"createdAtUtc":"2026-11-14T19:03:18.072Z"}
{"error":"customerId must be a non-empty string; lines[0].isbn must be a string; lines[0].quantity must be an integer between 1 and 20"} 400
{"error":"unknown ISBN 9780000000000"} 422
{"error":"Expected double-quoted property name in JSON at position 20 (line 1 column 21)"} 400
{"error":"content-type must be application/json"} 415
{"error":"request entity too large"} 413
{"error":"no route for DELETE /api/orders/ORD-1001"} 404
{"error":"Failed to decode param '%E0%A4'"} 400
{"error":"no route for GET /nope"} 404

Davranış büyük ölçüde aynı, ama farklar var. Express her cevaba X-Powered-By: Express ve JSON cevaplara zayıf bir ETag ekliyor; ilkini 15'te kapatacağız. Bozuk JSON'da artık V8'in (Node'un JavaScript engine'i) parse mesajı client'a gidiyor. Bizim router'ımızın verdiği 405 yok: Express, path eşleşip method eşleşmediğinde de 404 dönüyor. Bozuk path escape'ini Express kendisi 400'e çeviriyor. Kapaklar da express.static ile servis ediliyor; Cache-Control: public, max-age=86400, ETag ve Last-Modified header'ları hazır geliyor, ../ içeren path'ler 404 alıyor.

Express 4'ten Değişenler

Express 5.0 Eylül 2024'te çıktı, 5.3.0 Ekim 2026'da. Önce Tahmin Et'te ve path syntax'ında gördüklerimiz dışındaki başlıca değişiklikler:

  • Kaldırılanlar: app.del, req.param(), res.sendfile (yerine res.sendFile), res.json(obj, status) ve res.send(status) imzaları (yerine res.status(...).json(...) ve res.sendStatus), res.redirect("back"); acceptsEncoding gibi tekil method'lar çoğul adlarıyla kaldı.
  • Değişenler: req.query artık salt okunur bir getter ve varsayılan query parser "simple" (?a[b]=1 artık iç içe object'e dönüşmüyor), express.urlencoded'ın extended varsayılanı false, res.status() yalnız 100–999 arası tam sayı kabul ediyor, express.static nokta ile başlayan dosya ve klasörleri varsayılan olarak yok sayıyor, en az Node 18 gerekiyor.

Bir değişiklik özellikle sinsi: app.listen, EADDRINUSE gibi hataları artık fırlatmıyor, callback'e argüman olarak veriyor. Express 4 alışkanlığıyla yazılmış bir callback argümanına bakmaz:

import express from "express";

// Express 4 alışkanlığı: callback argümanını yok sayıyor
const app = express();
app.listen(3000, "127.0.0.1", () => console.log("listening on 3000"));
// Express 5 listen hatalarını (EADDRINUSE...) fırlatmak yerine bu callback'e verir
const server = app.listen(port, host, (err?: Error) => {
    if (err) {
        console.error(`cannot listen on ${host}:${port}: ${err.message}`);
        process.exit(1);
    }
    console.log(`Bookstore API listening on http://${host}:${port} (${app.get("env")})`);
});

// app.listen node:http server'ını döndürür; 10'daki timeout'lar burada da geçerli
server.headersTimeout = 10_000;
server.requestTimeout = 30_000;
server.keepAliveTimeout = 5_000;

Port 3000 doluyken ikisini çalıştırdım:

node labs/11/listen-unchecked.ts; echo "exit=$?"
node src/server.ts; echo "exit=$?"
listening on 3000
exit=0
cannot listen on 127.0.0.1:3000: listen EADDRINUSE: address already in use 127.0.0.1:3000
exit=1

İlki "listening" yazıp exit code 0 ile kapandı: dinleyen bir socket olmadığı için event loop'ta bekleyen iş kalmadı. Bir supervisor bunu başarılı bir çıkış sanabilir. Bu yazı için yük testi yaparken ben de buna takıldım: eski bir sunucu port'u tutuyordu, yenisi sessizce kapanmıştı ve ölçüm eski sunucuya gidiyordu.

Express'in destek sayfası bugün 4.x için de 5.x için de destek bitiş tarihini "ongoing" olarak gösteriyor; 4.22.3 Eylül 2026'da çıktı. Yani Express 4 bir EOL (end of life; desteğin bittiği tarih) takvimine bağlanmış değil, ama yeni özellikler 5'e geliyor. Geçiş için Express ekibinin bir codemod'u var: npx codemod@latest @expressjs/v5-migration-recipe (bu yazıda kullanmadım; bizim kodumuz doğrudan 5'e yazıldı).

NODE_ENV=production Express'te Neyi Değiştirir?

02'de NODE_ENV'in Express'te ölçülebilir bir etkisi olup olmadığını burada ölçeceğimizi söylemiştik. Önce Express'in ondan ne okuduğuna bakalım:

import express from "express";

// NODE_ENV bir Express uygulamasında neyi değiştiriyor?
const app = express();
console.log({ env: app.get("env"), viewCache: app.enabled("view cache"), etag: app.get("etag"), xPoweredBy: app.enabled("x-powered-by") });
{
  env: 'development',
  viewCache: false,
  etag: 'weak',
  xPoweredBy: true
}
{ env: 'production', viewCache: true, etag: 'weak', xPoweredBy: true }

Ayarlarda değişen tek şey view cache: template render eden uygulamalarda template'ler her istekte yeniden derlenmiyor. Bizim JSON API'mizde template yok. İkinci etki varsayılan hata sayfası. Önce Tahmin Et'teki uygulamayı NODE_ENV=production ile çalıştırınca cevap şuydu:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Internal Server Error</pre>
</body>
</html>

status 500

Stack trace gitti. Bookstore'un kendi hata handler'ı zaten ayrıntı vermiyor, ama hata handler'ı olmayan ya da ondan önce çöken bir uygulamada NODE_ENV'i unutmak dosya yollarını ve bağımlılık adlarını dışarı sızdırır. Son olarak performans: labs/11/load.ts 50 eşzamanlı döngüyle 10 saniye boyunca GET /api/books çağırıyor. API'yi iki modda üçer kez, sırayla değiştirerek çalıştırdım:

development run 1 [(development)]: 45419 req/s, p50 1.0 ms, p99 3.4 ms, errors 0
production run 1 [(production)]: 49316 req/s, p50 1.0 ms, p99 2.0 ms, errors 0
development run 2 [(development)]: 45509 req/s, p50 1.0 ms, p99 3.3 ms, errors 0
production run 2 [(production)]: 44400 req/s, p50 1.0 ms, p99 3.1 ms, errors 0
development run 3 [(development)]: 49150 req/s, p50 1.0 ms, p99 1.9 ms, errors 0
production run 3 [(production)]: 46455 req/s, p50 1.0 ms, p99 2.7 ms, errors 0

İki mod da 44–49 bin istek/saniye aralığında; farklar koşudan koşuya değişen gürültüden büyük değil. Yük üreten client da aynı makinede çalıştığı için bu sayılar mutlak bir kapasite değil, yalnız iki modun karşılaştırması. Sonuç: JSON API'de NODE_ENV=production Express'i hızlandırmıyor. Sık tekrarlanan "production'da birkaç kat hızlanır" önerisi template cache'inden geliyor. Yine de production'da NODE_ENV=production ver: hata sayfası için ve kendi kodunla bağımlılıkların bu değere bakabileceği için. Configuration'ı NODE_ENV'e bağlamak ise ayrı bir konu; 14'te environment variable'larla tipli bir configuration kuracağız.

Express mi, Fastify mi, NestJS mi, Hono mu?

Bu seride Express kullanıyoruz: en yaygını, middleware ekosistemi en geniş olanı ve öğrendiğin kavramlar diğerlerine taşınıyor. Alternatifler farklı ihtiyaçlara cevap veriyor (sürümler npm registry'den, 2026-11-14; bu çerçeveleri bu yazıda çalıştırmadım):

FrameworkYaklaşımNe zaman
Express 5.3Minimal, middleware tabanlı; yapıyı sen kurarsınKüçük ve orta API'ler, BFF (Backend for Frontend; bir UI'a özel API katmanı), mevcut middleware'lerden yararlanmak
Fastify 5.12JSON Schema ile validation ve serialization, plugin sistemiYüksek istek hacmi, şemanın baştan tanımlandığı API'ler
NestJS 12.5Angular'a benzer module, DI (dependency injection; bağımlılıkların dışarıdan verilmesi) ve decorator yapısı; altında Express ya da FastifyBüyük ekip, büyük domain; ASP.NET Core'daki yapıyı arayanlar
Hono 4.13Web standardı Request/Response üzerine küçük bir routerEdge runtime'lar, Bun ve Deno ile aynı kod

NestJS decorator kullandığı için 05'teki build'siz TypeScript yaklaşımıyla çalışmaz; bir build adımı gerekir. ASP.NET Core'dan gelen biri için en tanıdık olanı NestJS, en az şaşırtanı Express.

Üretimde Dikkat

listen callback'inde hatayı kontrol et. Express 5'te bunu yapmayan bir sunucu port doluyken başarıyla kapanmış gibi görünür.

Hata handler'ını en sona koy ve her zaman dört parametreyle yaz. Body parser'ın ve diğer middleware'lerin hatalarının status alanına bak; her şeyi 500'e çevirme.

Hata mesajlarını filtrele. Bozuk JSON'da V8'in mesajı, 500'de stack trace client'a gitmemeli; 12'de Problem Details formatıyla bunu düzenleyeceğiz.

Bağımlılık ağacını izle. Express 66 paketlik bir production ağacı getirdi; npm audit ve 04'teki kontroller artık gerçekten iş görüyor.

Sık Düşülen Tuzaklar

Error middleware'ini üç parametreyle yazmak. Express onu normal middleware sayar ve hatalar ona hiç ulaşmaz.

Middleware'i route'tan sonra eklemek. express.json() route'tan sonra kayıtlıysa req.body undefined olur; Önce Tahmin Et'teki hata bu.

Express 4 path syntax'ını taşımak. *, ? ve parametre içi RegEx Express 5'te açılışta hata verir.

NODE_ENV'i production'da vermemek. Varsayılan hata sayfası stack trace ve dosya yolları gösterir.

Async hata için hâlâ try/catch ve next(err) yazmak. Express 5'te gerek yok; ama Express 4'te kalan bir projede bu satırlar hayati.

Küçük Bir Deneme

★☆☆ labs/11/paths.ts'e /covers/{*file} route'unu ekle ve /covers/ path'ini dene. *file ile farkı ne?

★★☆ API'ye, path eşleşip method eşleşmediğinde 405 ve Allow header'ı dönen bir middleware ekle. Hangi bilgiye ihtiyacın var ve onu Express'ten nasıl alırsın?

★★★ labs/11/load.ts'i GET /api/orders/ORD-1001 ve export endpoint'iyle (rows=1000) çalıştır. NODE_ENV yine fark etmiyor mu? requestContext'teki log satırını kapatınca istek/saniye ne kadar değişiyor?

Express iskeleti hazır. Bir sonraki yazıda bu iskeleti gerçek bir API'ye dönüştürüyoruz: routes → handler → service → repository katmanları, zod ile request şeması ve tip türetme, Problem Details (RFC 9457) formatında hata cevapları ve fiyatları kuruş cinsinden tam sayı olarak tutmak.