Node.js HTTP Sunucusu ile Kendi Modüler HTTP Uygulamanızı Geliştirme Rehberi
Node.js, asenkron ve olay tabanlı mimarisi sayesinde sunucu tarafı uygulamalar geliştirmek için popüler bir platform haline gelmiştir. Genellikle Express.js gibi framework’ler aracılığıyla web uygulamaları geliştirilse de, Node.js’in kendi bünyesindeki http modülü, HTTP sunucularını en temel seviyede anlamak ve kontrol etmek için güçlü bir temel sunar. Bu makale, Node.js’in yerleşik HTTP sunucusunu kullanarak nasıl modüler ve ölçeklenebilir bir HTTP uygulaması oluşturabileceğinizi adım adım açıklayacaktır. Kendi HTTP modülünüzü inşa etmek, framework’lerin arkasındaki mekanizmaları kavramanıza ve daha optimize edilmiş, ihtiyaca özel çözümler geliştirmenize olanak tanır.
Node.js HTTP Sunucusuna Derinlemesine Bakış
Node.js, JavaScript’i sunucu tarafında çalıştırmamızı sağlayan bir çalışma zamanı ortamıdır. Web uygulamalarının temelini oluşturan HTTP protokolü ile etkileşim kurmak için güçlü ve esnek bir http modülü sunar. Bu bölüm, bu modülün temel bileşenlerini ve nasıl çalıştığını detaylandıracaktır.
http Modülünün Temelleri
http modülü, Node.js’in çekirdek modüllerinden biridir ve herhangi bir ek kurulum gerektirmez. Bu modül, HTTP istemcisi ve sunucusu işlevselliği sağlar. Bir web sunucusu oluşturmak için genellikle http.createServer() metodu kullanılırken, harici bir API’ye istek göndermek için http.request() veya http.get() metotları tercih edilir. Kendi HTTP modülümüzü oluştururken, bu temel işlevsellikleri kullanarak isteklere yanıt vereceğiz ve sunucumuzun davranışını özelleştireceğiz.
createServer Metodu ve Olay Tabanlı Mimari
http.createServer() metodu, bir HTTP sunucusu nesnesi döndürür. Bu metoda genellikle bir geri çağırma (callback) fonksiyonu argüman olarak verilir. Bu geri çağırma fonksiyonu, sunucuya her yeni istek geldiğinde çalıştırılır ve iki ana parametre alır: request (gelen istek) ve response (gönderilecek yanıt). Node.js’in olay tabanlı mimarisi sayesinde, bu sunucu nesnesi çeşitli olayları (örneğin, 'request', 'connection', 'close') dinleyebilir ve bu olaylar tetiklendiğinde belirli fonksiyonları çalıştırabilir.
const http = require('http');
const server = http.createServer((req, res) => {
// Her istek geldiğinde bu fonksiyon çalışır
console.log(Gelen İstek: ${req.method} ${req.url});
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Merhaba Dunya!\n');
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(Sunucu http://localhost:${PORT} adresinde çalışıyor);
});
request ve response Objelerinin Anatomisi
Geri çağırma fonksiyonundaki req (IncomingMessage) ve res (ServerResponse) objeleri, gelen isteği işlemek ve yanıtı oluşturmak için kritik öneme sahiptir.
request(IncomingMessage) Objesi: Gelen HTTP isteği hakkında tüm bilgileri içerir. Önemli özellikleri arasındareq.url(isteğin URL'si),req.method(HTTP metodu: GET, POST vb.),req.headers(istek başlıkları) vereq.body(istek gövdesi, akış olarak okunur) bulunur.response(ServerResponse) Objesi: İstemciye geri gönderilecek yanıtı oluşturmak için kullanılır. Önemli metotları arasındares.statusCode(HTTP durum kodu),res.setHeader()(yanıt başlığı ayarlama) veres.end()(yanıtı sonlandırma ve gönderme) yer alır.
Basit Bir HTTP Modülü Yapılandırma
Şimdi temel bilgileri edindiğimize göre, Node.js'in http modülünü kullanarak basit bir HTTP modülü oluşturmaya başlayabiliriz. Bu bölüm, temel bir sunucu kurulumundan, gelen istekleri işlemeye ve URL ile HTTP metotlarını kullanmaya kadar olan adımları kapsayacaktır.
Temel Sunucu Oluşturma ve Dinleme
Yukarıdaki örnekte gösterildiği gibi, http.createServer() ile bir sunucu oluşturup server.listen() ile belirli bir portta dinlemeye başlamak, her Node.js HTTP uygulamasının başlangıç noktasıdır. Sunucuyu dinlemeye almak, gelen bağlantıları kabul etmeye hazır hale getirmek anlamına gelir. Genellikle localhost (127.0.0.1) ve belirli bir port (örneğin 3000 veya 8080) kullanılır.
İstekleri İşleme ve Yanıt Gönderme
Sunucuya gelen her istek, createServer metoduna verilen geri çağırma fonksiyonu tarafından işlenir. Bu fonksiyon içinde, request objesinden gelen bilgileri okuyarak (URL, metot, başlıklar vb.) ve response objesini kullanarak uygun bir yanıt oluştururuz. Yanıtın içeriğini (Content-Type) ve durum kodunu (statusCode) ayarlamak önemlidir.
// ... (http modülü ve server.listen kısmı)
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('Ana Sayfa
Hoş geldiniz!
');
} else if (req.url === '/hakkinda') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('Hakkımızda
Bu bir Node.js HTTP modülüdür.
');
} else {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('404 Sayfa Bulunamadı');
}
});
URL ve HTTP Metotlarını Kullanma
req.url ve req.method, gelen isteği farklı yollarla işlemek için en temel araçlardır. req.url, isteğin yolunu (path) ve sorgu dizgisini (query string) içerir. req.method ise isteğin HTTP metodunu (GET, POST, PUT, DELETE vb.) belirtir. Bu bilgilerle, uygulamanızın farklı URL'lere ve farklı HTTP metotlarına nasıl yanıt vereceğini belirleyebilirsiniz. Örneğin, /api/users adresine gelen bir GET isteği kullanıcı listesini döndürürken, aynı adrese gelen bir POST isteği yeni bir kullanıcı oluşturabilir.
Gelişmiş İstek Yönlendirme (Routing) Mekanizmaları
Uygulamalar büyüdükçe, her URL'yi ve metodu if-else bloklarıyla yönetmek sürdürülemez hale gelir. Bu noktada, istek yönlendirme (routing) mekanizmalarına ihtiyaç duyarız. Bu bölüm, kendi basit yönlendirme çözümümüzü nasıl oluşturacağımızı ve parametreli URL'leri nasıl işleyeceğimizi anlatır.
Manuel Yönlendirme Yaklaşımları
Yukarıdaki örnekte gösterilen if-else zinciri, manuel yönlendirmeye bir örnektir. Daha karmaşık senaryolarda, bu yapıyı daha düzenli hale getirmek için bir dizi obje veya fonksiyon kullanabiliriz. Örneğin, her bir rotayı (route) bir obje olarak tanımlayıp, bu objeleri bir dizide tutarak istek geldiğinde uygun rotayı arayabiliriz.
Basit Bir Router Modülü Oluşturma
Uygulamanın ana dosyasını temiz tutmak için, yönlendirme mantığını ayrı bir modüle taşımak iyi bir pratiktir. Bu modül, gelen isteğin URL'sini ve metodunu alarak, uygun işleyici fonksiyonu (handler) bulur ve çalıştırır.
// router.js
const routes = {
'GET /': (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('Ana Sayfa
');
},
'GET /api/users': (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify([{ id: 1, name: 'Ali' }, { id: 2, name: 'Veli' }]));
},
'POST /api/users': (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const newUser = JSON.parse(body);
res.statusCode = 201;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Kullanıcı oluşturuldu', user: newUser }));
});
}
};
function handleRoute(req, res) {
const routeKey = ${req.method} ${req.url};
const handler = routes[routeKey];
if (handler) {
handler(req, res);
} else {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('404 Sayfa Bulunamadı');
}
}
module.exports = handleRoute;
// server.js
const http = require('http');
const router = require('./router'); // Kendi router modülümüzü import ediyoruz
const server = http.createServer(router); // router fonksiyonunu doğrudan callback olarak veriyoruz
const PORT = 3000;
server.listen(PORT, () => {
console.log(Sunucu http://localhost:${PORT} adresinde çalışıyor);
});
Parametreli URL'ler ve Sorgu Dizileri
Gerçek dünya uygulamalarında URL'ler genellikle parametreler içerir (örn: /users/123). Kendi router'ımızda bu tür parametreleri yakalamak için URL ayrıştırma (parsing) işlemleri yapmamız gerekir. Node.js'in url modülü bu konuda yardımcı olabilir. Ayrıca, req.url içinde yer alan sorgu dizgilerini (?name=Ali&age=30) ayrıştırmak için URLSearchParams API'si veya querystring modülü kullanılabilir.
// router.js (güncellenmiş)
const url = require('url');
const routes = {
// ... diğer rotalar
'GET /api/users/:id': (req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathSegments = parsedUrl.pathname.split('/').filter(Boolean); // ['api', 'users', '123']
const userId = pathSegments[2]; // '123'
// userId ile ilgili işlemi yap
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ id: userId, name: Kullanıcı ${userId} }));
}
};
function handleRoute(req, res) {
const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const method = req.method;
// Dinamik rotaları kontrol etmek için daha karmaşık bir mantık
for (const routeKey in routes) {
const [routeMethod, routePath] = routeKey.split(' ');
if (routeMethod === method) {
// Basit bir regex veya string karşılaştırma ile parametreleri yakalama
const routeRegex = new RegExp(^${routePath.replace(/:[a-zA-Z0-9_]+/g, '([a-zA-Z0-9_]+)')}$);
const match = path.match(routeRegex);
if (match) {
// Parametreleri req objesine ekleyebiliriz
// req.params = { id: match[1] }; // Örnek
routes[routeKey](req, res);
return;
}
}
}
// Eğer eşleşen rota bulunamazsa
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('404 Sayfa Bulunamadı');
}
// ...
Middleware Kavramı ve Uygulaması
Middleware (ara yazılım), bir isteğin sunucuya ulaşmasından ve bir yanıtın istemciye gönderilmesinden önce veya sonra belirli işlemleri gerçekleştiren fonksiyonlardır. Kimlik doğrulama, loglama, veri ayrıştırma gibi görevler için çok kullanışlıdırlar. Express.js gibi framework'lerde yaygın olarak kullanılırken, kendi HTTP modülümüzde de benzer bir yapıyı oluşturabiliriz.
Middleware Nedir ve Neden Kullanılır?
Middleware fonksiyonları, gelen isteği işleyen ana işleyiciye ulaşmadan önce veya sonra çalışır. Her middleware fonksiyonu, isteği işleyebilir, yanıtı değiştirebilir ve isteği bir sonraki middleware'e veya nihai işleyiciye iletebilir. Bu, kod tekrarını azaltır, modülerliği artırır ve uygulamanın farklı katmanlarında ortak işlevselliği kolayca yönetmeyi sağlar.
- Loglama: Her isteği kaydetmek.
- Kimlik Doğrulama: Kullanıcının yetkili olup olmadığını kontrol etmek.
- Vücut Ayrıştırma (Body Parsing): POST/PUT isteklerindeki JSON veya form verilerini ayrıştırmak.
- Hata Yönetimi: Uygulama genelinde hataları yakalamak.
Kendi Basit Middleware Fonksiyonlarınızı Yazma
Bir middleware fonksiyonu genellikle (req, res, next) parametrelerini alır. req ve res bilinen istek ve yanıt objeleridir. next ise, bir sonraki middleware fonksiyonunu veya nihai rota işleyicisini çağırmak için kullanılan bir fonksiyondur. next() çağrılmazsa, istek zinciri durur.
// loggerMiddleware.js
function loggerMiddleware(req, res, next) {
console.log([${new Date().toISOString()}] ${req.method} ${req.url});
next(); // Bir sonraki middleware'e veya rota işleyicisine geç
}
module.exports = loggerMiddleware;
// authMiddleware.js
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (token === 'Bearer mysecrettoken') {
// req.user = { id: 1, role: 'admin' }; // Kullanıcı bilgilerini ekleyebiliriz
next();
} else {
res.statusCode = 401;
res.setHeader('Content-Type', 'text/plain');
res.end('Yetkisiz Erişim');
}
}
module.exports = authMiddleware;
Middleware Zincirleri Oluşturma
Birden fazla middleware'i sırayla çalıştırmak için bir zincir oluşturabiliriz. Ana sunucu fonksiyonumuzda, her isteği bu middleware zincirinden geçirebiliriz.
// app.js (ana uygulama dosyası)
const http = require('http');
const url = require('url');
const loggerMiddleware = require('./loggerMiddleware');
const authMiddleware = require('./authMiddleware');
// Basit bir middleware yöneticisi
const middlewares = [];
function use(middleware) {
middlewares.push(middleware);
}
// Middleware'leri tanımla
use(loggerMiddleware);
// use(authMiddleware); // İsteğe bağlı olarak kimlik doğrulama ekle
const routes = {
'GET /': (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('Ana Sayfa
');
},
'GET /protected': (req, res) => {
// Bu rotaya erişim için authMiddleware'den geçmek gerekir
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Korumalı Alan!');
}
};
const server = http.createServer((req, res) => {
let middlewareIndex = 0;
const next = () => {
if (middlewareIndex < middlewares.length) {
middlewares[middlewareIndex++](req, res, next);
} else {
// Tüm middleware'ler tamamlandı, şimdi rotayı işle
const parsedUrl = url.parse(req.url, true);
const routeKey = ${req.method} ${parsedUrl.pathname};
const handler = routes[routeKey];
if (handler) {
handler(req, res);
} else {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/plain');
res.end('404 Sayfa Bulunamadı');
}
}
};
next(); // Middleware zincirini başlat
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(Sunucu http://localhost:${PORT} adresinde çalışıyor);
});
Modüler Yapıyı Güçlendirme ve Bakım
Büyük uygulamalarda tüm kodun tek bir dosyada olması yönetilemez hale gelir. Node.js'in modül sistemi (CommonJS), kodu ayrı dosyalara bölerek daha düzenli, okunabilir ve bakımı kolay bir yapı oluşturmamızı sağlar.
Kodun Modüllere Ayrılması
Her bir işlevselliği (örneğin, router, middleware, veritabanı işlemleri, yardımcı fonksiyonlar) kendi ayrı dosyasına taşımak en iyi yaklaşımdır. Her dosya, module.exports kullanarak dışarıya bir fonksiyon, obje veya sınıf ihraç eder. Diğer dosyalar ise require() fonksiyonu ile bu ihraç edilen modülleri içe aktarır.
Örneğin, routes.js, middlewares.js, controllers.js gibi dosyalar oluşturarak kodu daha iyi organize edebiliriz. Bu, her bir modülün belirli bir sorumluluğa sahip olmasını sağlar ve uygulamanın genel mimarisini basitleştirir.
Farklı Modülleri Birleştirme
Ana uygulama dosyanız (genellikle app.js veya server.js), tüm bu modülleri bir araya getiren orkestratör görevi görür. Burada middleware'ler uygulanır, rotalar tanımlanır ve sunucu başlatılır. Bu yapı, her bir bileşenin bağımsız olarak geliştirilip test edilmesine olanak tanır.
Yapısal En İyi Uygulamalar
- Sorumlulukların Ayrımı (Separation of Concerns): Her modülün tek bir işi yapmasını sağlayın.
- Tutarlı İsimlendirme: Dosya ve değişken isimlerinde tutarlılık kullanın.
- Dizin Yapısı: Modülleri mantıksal olarak gruplandıran bir dizin yapısı oluşturun (örn:
/routes,/middlewares,/controllers,/utils). - Küçük Modüller: Mümkün olduğunca küçük ve odaklanmış modüller yazın.
Hata Yönetimi, Güvenlik ve Performans İpuçları
Sağlam bir HTTP uygulaması sadece işlevsel olmakla kalmaz, aynı zamanda hataları düzgün bir şekilde yönetmeli, güvenlik açıklarına karşı korunmalı ve iyi bir performans sunmalıdır.
Sağlam Hata Yönetimi Stratejileri
Node.js'te hatalar asenkron doğaları nedeniyle bazen zorlayıcı olabilir. Sunucu tarafında oluşabilecek hataları (örn: dosya okuma hatası, veritabanı bağlantı hatası) yakalamak ve istemciye uygun bir hata yanıtı göndermek önemlidir.
try-catchBlokları: Senkron kod bloklarındaki hataları yakalamak için kullanılır.- Promise
.catch(): Promise tabanlı asenkron işlemlerin hatalarını yönetmek için kullanılır. - Global Hata Yakalayıcılar: Yakalanmayan hatalar için
process.on('uncaughtException')veprocess.on('unhandledRejection')gibi olay dinleyicileri kullanılabilir. Bu, uygulamanın çökmesini önlemek ve temiz bir şekilde kapanmasını sağlamak için önemlidir. - Özel Hata Middleware'i: Tüm hata durumlarını merkezi bir noktada ele almak için özel bir hata middleware'i oluşturulabilir.
Temel Güvenlik Önlemleri
Kendi HTTP modülünüzü geliştirirken temel güvenlik adımlarını göz ardı etmemelisiniz:
- Giriş Doğrulama (Input Validation): Kullanıcıdan gelen tüm girişleri her zaman doğrulayın ve sanitize edin.
- HTTP Başlıkları: Güvenlik ile ilgili HTTP başlıklarını (örn:
X-Content-Type-Options,X-Frame-Options,Content-Security-Policy) ayarlayın. - Hassas Veri Yönetimi: Parolalar gibi hassas verileri şifreleyin ve güvenli bir şekilde saklayın.
- Rate Limiting: DDoS saldırılarını veya brute-force denemelerini önlemek için istek hız sınırlaması uygulayın.
- Güvenlik Açıkları: SQL Enjeksiyonu, XSS (Cross-Site Scripting), CSRF (Cross-Site Request Forgery) gibi yaygın güvenlik açıklarına karşı önlemler alın.
Performans İyileştirmeleri ve Ölçeklenebilirlik
Node.js, tek iş parçacıklı yapısı nedeniyle CPU yoğun işlemler için doğrudan uygun değildir. Ancak doğru stratejilerle yüksek performanslı ve ölçeklenebilir uygulamalar geliştirebilirsiniz:
- Asenkron İşlemler: Blocking (engelleyici) operasyonlardan kaçının. Tüm I/O (giriş/çıkış) işlemlerini asenkron olarak yapın.
- Kümeleme (Clustering): Node.js'in
clustermodülünü kullanarak birden fazla CPU çekirdeğini kullanabilir ve uygulamanızın performansını artırabilirsiniz. - Yük Dengeleme (Load Balancing): Birden fazla uygulama örneğini bir yük dengeleyici arkasına yerleştirerek gelen istekleri dağıtın.
- Önbellekleme (Caching): Sık erişilen verileri önbelleğe alarak veritabanı veya diğer kaynaklara yapılan gereksiz çağrıları azaltın.
- Statik Dosya Sunumu: Resimler, CSS, JavaScript gibi statik dosyaları doğrudan Node.js sunucusu yerine Nginx gibi optimize edilmiş bir web sunucusu üzerinden sunun.
Sonuç
Bu makalede, Node.js'in yerleşik http modülünü kullanarak nasıl modüler bir HTTP uygulaması oluşturulacağını detaylı bir şekilde inceledik. Temel sunucu kurulumundan, istek yönlendirmeye, middleware kullanımına ve modüler yapılandırmaya kadar birçok konuyu ele aldık. Kendi HTTP modülünüzü inşa etmek, Node.js'in altında yatan mekanizmaları daha iyi anlamanızı sağlar ve size framework'lerin sunduğu kolaylıkların ötesinde daha fazla kontrol ve esneklik sunar. Bu yaklaşım, performans kritik uygulamalar veya öğrenme amaçlı projeler için paha biçilmez bir deneyim sağlar. Temel bilgileri ve en iyi uygulamaları takip ederek, sağlam, güvenli ve ölçeklenebilir Node.js HTTP uygulamaları geliştirebilirsiniz.
SSS (Sık Sorulan Sorular)
Neden Express.js yerine kendi HTTP modülümüzü yazmalıyız?
Kendi HTTP modülünüzü yazmak, Node.js'in temel HTTP mekanizmalarını derinlemesine anlamanızı sağlar. Bu, framework'lerin nasıl çalıştığını kavramanıza yardımcı olur ve özel ihtiyaçlarınıza göre daha hafif, optimize edilmiş çözümler geliştirmenize olanak tanır. Ayrıca, framework bağımlılığını azaltır ve daha fazla kontrol sağlar.
Bu yaklaşımın dezavantajları nelerdir?
En büyük dezavantajı, Express.js gibi olgun framework'lerin sunduğu birçok hazır özelliği (örneğin, gelişmiş yönlendirme,