Web servislerine genel bakış: hangi katmanda ne kullanılır
"Hono mu Nitro mu Nginx mi" diye sorulduğunda verilecek doğru cevap yok, çünkü üçü aynı işi yapmıyor. Hono bir uygulama framework'ü, Nitro bir sunucu derleyicisi, Nginx ise uygulamanın önünde duran bir proxy. Aynı istek üçünün de içinden geçebilir.
Karşılaştırmaya girmeden önce katmanları ayırmak lazım, yoksa tartışma "hangisi daha hızlı" seviyesinde takılıyor ve o soru pratikte en az önemli olanı.
Bu yazıda kendi çalışma düzenimizi merkeze aldım: iş mantığı ağırlıklı servisler yazıyoruz ve bunları mümkün olduğunca ince bir arayüzle dışarı veriyoruz. Ağır bir SPA yok, kullanıcıya dönen şey çoğunlukla API ve üstüne oturan az sayıda ekran. Aşağıdaki tercihler bu senaryoya göre. Ağır etkileşimli bir ürün arayüzü yazıyorsan özellikle arayüz bölümündeki kararlar senin için farklı çıkar.
Bir isteğin geçtiği katmanlar
istemci
→ [ 1. giriş katmanı ] Nginx / Caddy / Traefik / gateway
→ [ 2. runtime ] Node / Bun / Deno / Workers
→ [ 3. uygulama ] Hono / Fastify / Elysia / Nitro / Express
İstek yukarıdan aşağı akıyor ama kararlar bu sırayla verilmiyor. Pratikte önce nereye deploy edeceğini bilmen gerekiyor, çünkü dağıtım hedefi runtime'ı, runtime da elindeki framework listesini daraltıyor. Cloudflare Workers'a gidiyorsan Fastify seçeneği zaten yok. Tek bir VPS'te duruyorsan edge tartışmasının tamamı senin için gereksiz.
Uygulama katmanı
Hono
Web standartlarının üstüne kurulu, yani Request ve Response nesneleriyle çalışıyor. Bunun sonucu şu: Node, Bun, Deno, Cloudflare Workers ve Lambda üzerinde aynı kod çalışıyor. Paket boyutu 14 KB civarında; Express'in 200 KB'ının yanında ciddi bir fark ve edge runtime'larda bu boyut doğrudan soğuk başlangıç süresine yansıyor.
import { Hono } from 'hono'
const app = new Hono()
app.get('/health', (c) => c.json({ ok: true }))
app.post('/invoices', async (c) => {
const body = await c.req.json()
const invoice = await createInvoice(body)
return c.json(invoice, 201)
})
export default app
Bu dosya değişmeden Workers'a da, Node'a da deploy edilebiliyor.
Öğrenmesi bir saat sürüyor ve TypeScript tarafı temiz. Ama "her yerde çalışır" iddiasını pratikte kendi kodun bozuyor: fs, Buffer, native bir kütüphane ya da uzun süren bir işlem eklediğin anda o handler'ın Workers'ta çalışmadığını görüyorsun. Taşınabilirlik framework'ün özelliği, senin uygulamanın değil. Express etrafında yıllar içinde birikmiş hazır middleware yığını da burada yok, bazı şeyleri kendin yazacaksın.
Küçük ve orta boy servisler, edge'e gidecek API'ler ve dağıtım hedefinin değişebileceğini düşündüğün işler için doğru tercih.
Fastify
Node üzerinde kalacaksan varsayılan tercihim bu. Değerini hızından çok şema tarafında gösteriyor: JSON Schema ile hem doğrulama hem serileştirme yapıyor, aynı şemadan OpenAPI çıkarabiliyorsun.
import Fastify from 'fastify'
const app = Fastify({ logger: true })
app.post('/invoices', {
schema: {
body: {
type: 'object',
required: ['customerId', 'amount'],
properties: {
customerId: { type: 'string' },
amount: { type: 'number', minimum: 0 },
},
},
response: {
201: {
type: 'object',
properties: { id: { type: 'string' }, amount: { type: 'number' } },
},
},
},
}, async (req, reply) => {
const invoice = await createInvoice(req.body)
return reply.code(201).send(invoice)
})
Buradaki response şeması dokümantasyondan fazlasını yapıyor: Fastify ona bakarak hızlı bir serileştirici derliyor ve şemada olmayan alanlar yanıta hiç çıkmıyor. Yanlışlıkla password_hash sızdırmak da böylece zorlaşıyor. Servisler arası konuşan bir mimaride bu, hız farkından daha değerli.
Eklenti sistemi olgun ve gerçek bir kapsülleme sunuyor, Node ekosisteminin tamamı da elinin altında kalıyor. Bedeli şu: Node'a bağlısın, edge runtime'larda çalışmıyor. Şema yazmak da başta seni yavaşlatıyor, karşılığını servis sayısı arttıkça alıyorsun.
Elysia
Bun için yazılmış ve Bun'ın karakteristiklerinden faydalanıyor. Tip çıkarımı bu listedeki en iyilerden biri: şema tanımından tipi kendisi üretiyor, aynı şeyi iki kez yazmıyorsun.
Karşılığında Bun'a bağlanıyorsun; bu bir sorun olmak zorunda değil ama runtime kararını framework kararına yapıştırmış oluyorsun, geri dönmek istediğinde ikisini birden değiştirmen gerekiyor. Bun'ı zaten benimsemişsen mantıklı, benimsememişsen sırf bunun için geçmeye değmez.
Express
35 milyon haftalık indirme ve bakım moduna geçmiş bir kod tabanı. Mevcut Express servislerini bugün taşımak için acele bir sebep yok, çalışıyorlarsa çalışsınlar. Ama yeni bir servise Express ile başlamak için de bir sebep kalmadı; yukarıdaki üç seçenek de daha fazlasını veriyor.
Nitro
Bu listedeki diğerleriyle aynı kategoride değil. Nitro bir framework'ten çok sunucu derleyicisi: yazdığın sunucu kodunu alıp hedefe göre paketliyor. 15'ten fazla hazır hedefi var; aynı proje Vercel'e, Cloudflare'a, Lambda'ya ya da düz bir Node sunucusuna (node_server) build edilebiliyor. Nuxt'ın altında çalışan şey de bu, ama Nuxt'sız da kullanılabiliyor.
Kazancını, dağıtım hedefini sonradan değiştirebilmende gösteriyor. Standart preset ile üretilen sunucu bundle'ı 10 KB'ın altında kalıyor, uyumluluk tarihleri (compatibility dates) sayesinde de davranış proje oluşturulduğu ana sabitleniyor ve sonradan bilinçli olarak güncelleniyor; uzun ömürlü servislerde bunun değeri var.
Bedeli, araç zincirine bir katman daha eklemek. Bir şey ters gittiğinde hata kendi kodunla derleyicinin ürettiği çıktı arasında bir yerde oluyor, bunu okumak Fastify'da stack trace okumaktan daha zahmetli. İnce bir arayüzle API'yi aynı projede tutup tek yere deploy edeceksen ya da nereye deploy edeceğine henüz karar vermediysen bu bedele değer.
İnce arayüzü nasıl servis etmeli
Bizim durumumuzda arayüz, ürünün kendisi yerine servisin görünen yüzü oluyor. Bu kabulü net koymak birçok kararı basitleştiriyor.
Üç yaygın yol var.
Statik dosya + ayrı API. Arayüzü build edip bir CDN'den ya da proxy'den veriyorsun, API ayrı bir adreste duruyor. Kurulumu en basit görünen seçenek ama faturayı oturum ve CORS tarafında ödüyorsun. İki farklı origin olduğu anda çerez ayarları, preflight istekleri ve token saklama sorunları başlıyor. Az sayıda ekran için bu bedel yüksek.
Aynı süreçten sunucu tarafı render. Nitro, Nuxt, Next ya da benzeri bir kurulumla arayüz ve API aynı yerde duruyor. Origin tek olduğu için oturum meselesi kendiliğinden çözülüyor. Karşılığında build zinciri ağırlaşıyor.
Sunucudan doğrudan HTML. Arayüz gerçekten inceyse en az hareketli parçaya sahip yol bu. Laravel tarafında Blade ya da Inertia, JS tarafında sunucudan dönen HTML üstüne az miktarda etkileşim. Ayrı bir frontend build'i, ayrı bir deploy adımı, ayrı bir bağımlılık ağacı olmuyor.
Şöyle bir eşik kullanıyorum: ekran sayısı bir elin parmaklarını geçmiyorsa ve çoğu form ile tablodan ibaretse, ayrı bir frontend servisi çıkarmak kazandırdığından fazlasını götürüyor. Ayrı frontend, ikinci bir dağıtım hattı, ikinci bir bağımlılık güncelleme yükü ve API ile arayüz arasında sürüm uyumsuzluğu ihtimali demek.
Yön seçerken şuna dikkat etmek gerekiyor: arayüz ilerde gerçekten büyürse birleşik yapıdan ayrı bir frontend'e geçmek mümkün, çünkü API zaten ortada duruyor. Tersi, yani baştan iki servis kurup sonra tek yere toplamak çok daha zahmetli. O yüzden emin değilsen birleşik başlamak daha ucuz.
Giriş katmanı
Nginx
Yirmi yıllık bir yazılım ve web sitelerinin üçte birinden fazlası bunun üstünde çalışıyor. Statik dosya servisinde ve ters vekil olarak en hızlısı, davranışı öngörülebilir, karşılaşacağın her sorunun cevabı bir yerlerde yazılı.
Buna karşılık yapılandırmayı elle yazıyorsun ve dosya servis eklendikçe büyüyor. TLS sertifikaları ayrı bir iş, certbot ya da benzeri bir şey kurmak gerekiyor. Servisler dinamik olarak gelip gidiyorsa Nginx bunu kendiliğinden öğrenmiyor, her seferinde yapılandırmaya dokunman lazım. Topoloji sabitse, statik dosya trafiği yoğunsa ya da ekipte zaten Nginx bilen biri varsa hâlâ en sağlam seçenek.
Caddy
Otomatik HTTPS'i varsayılan davranış olarak sunuyor; alan adını yazıyorsun, sertifika işi kendiliğinden hallediliyor. Yapılandırma dosyası okunabilir:
api.ornek.com {
reverse_proxy localhost:3000
}
ornek.com {
root * /var/www/ui
file_server
try_files {path} /index.html
}
Nginx'te aynı işi yapan blok bunun birkaç katı uzunlukta olur ve içinde sertifika yolları bulunur.
Sertifika yönetimi tamamen ortadan kalkıyor ve tek binary ile çalışıyorsun. Çok yüksek trafikte Nginx kadar ince ayar yapamıyorsun, eklenti ekosistemi de daha küçük. Tek sunucu ya da birkaç servisle çalışıyorsan, yani yazının başında tarif ettiğim türden bir kurulumda, çoğu durumda doğru varsayılan bu.
Traefik
Docker etiketlerinden ve Kubernetes kaynaklarından servisleri kendiliğinden keşfediyor. Yeni bir servis ayağa kaldırdığında proxy yapılandırmasına dokunmuyorsun:
services:
invoices:
image: ghcr.io/ornek/invoices:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.invoices.rule=Host(`api.ornek.com`) && PathPrefix(`/invoices`)"
- "traefik.http.services.invoices.loadbalancer.server.port=3000"
Rate limit, kimlik doğrulama ve devre kesici gibi ara katmanlar da hazır geliyor. Bunun bedeli, yapılandırmanın etiketlere dağılması: sistemin bütününü tek dosyada görmek zorlaşıyor. Değişikliklerin yayılması Kong ya da Istio'ya kıyasla biraz daha yavaş, ki bu ancak çok büyük kurulumlarda hissedilen bir fark. Docker Compose ya da Kubernetes ile çalışıyorsan ve servisler sık deploy ediliyorsa aradığın şey bu.
Gerçek gateway'ler: Kong, Envoy, APISIX
Bunlar farklı bir ihtiyaca cevap veriyor: API anahtarı yönetimi, kota, plan bazlı sınırlama, dış geliştiricilere açılan API'ler, servis mesh veri düzlemi. Envoy çok yüksek ölçekte ve mesh içinde (Istio'nun altında da o var) anlamlı.
On beş servisi olan bir ekip için bunlar fazla. Kong'un ya da Envoy'un asıl kazancı, API'yi dışarıya ürün olarak satıyorsan veya birden fazla ekibin ortak bir giriş politikası paylaşması gerekiyorsa ortaya çıkıyor. Elli servisi Traefik ile 30 MB bellekle yöneten kurulumlar var; ihtiyacın bu değilse gateway katmanı sana sadece bakım yükü olarak geri döner.
Runtime tarafı
Node. Varsayılan. Milyarlarca saatlik üretim geçmişi, ekosistemin tamamı, olgun profil ve izleme araçları. Karmaşık iş mantığı ve çok sayıda veritabanı çağrısı içeren yüklerde gecikme davranışı öngörülebilir.
Bun. Ham hız tarafında önde ve uyumluluk sorunlarının çoğu 1.2 ve 1.3 ile kapandı. Node'dan Bun'a taşınıp gecikmeyi ciddi biçimde düşüren ekipler var. Buna karşılık ağır I/O ve karmaşık iş mantığı altında kuyruk gecikmesi (yani en yavaş yüzdelik dilimler) Node kadar istikrarlı olmayabiliyor. Ortalama yanıt süresine bakıp karar verme, p99'a bak.
Deno. İzin tabanlı güvenlik modeli ve varsayılan olarak TypeScript. Güvenlik sınırlarının önemli olduğu ve edge'e yakın işlerde mantıklı.
Cloudflare Workers ve benzerleri. Soğuk başlangıç pratikte yok, coğrafi dağıtım hazır geliyor. Karşılığında CPU süresi sınırlı, Node API'lerinin bir kısmı yok, uzun süren veya bellek yiyen işler buraya sığmıyor. Ölçüt basit: istek başına iş küçük ve gecikme kritikse iyi, rapor üretmek ya da dosya işlemek gerekiyorsa değil.
Runtime seçimini framework seçiminden önce yapmak, tersini yapmaktan daha az pişmanlık üretiyor.
Mikroservis mi, tek servis mi
Bu tartışmada dürüst olmak gerekiyor: çoğu ekip için mikroservis gereksiz ve pahalı.
Bölmek şu durumlarda gerçekten işe yarıyor:
- Parçaların ölçek profili farklıysa. Bir uç nokta günde milyonlarca istek alıp diğeri yüz tane alıyorsa, ikisini ayrı ölçeklemek para kazandırıyor.
- Farklı dil ya da runtime gerekiyorsa. Görüntü işleme Go'da, geri kalan TypeScript'te olabilir.
- Deploy ritimleri ayrışıyorsa. Ödeme servisi ayda bir, kampanya servisi günde üç kez deploy ediliyorsa ayırmak ikisini de rahatlatıyor.
- Ekip sınırı varsa. Ayrı ekipler ayrı deploy hattı istiyorsa, bu teknik değil örgütsel bir gerekçe ama en geçerlilerinden.
Bölmemek gereken durumlar:
- Tek ekip, tek veritabanı. Servisleri ayırıp aynı veritabanına yazdırıyorsan mikroservis yapmış olmuyorsun, dağıtık monolit yapmış oluyorsun ve iki yaklaşımın da kötü taraflarını alıyorsun.
- "Daha temiz olur" gerekçesi. Modül sınırı çizmek için ağ sınırı çizmek gerekmiyor, aynı süreç içinde de sınır çizebilirsin.
Bölmeye karar verdiysen üç şey en baştan netleşmeli. Birincisi, servisler senkron HTTP ile mi konuşacak yoksa kuyruk üzerinden mi; senkron zincir uzadıkça bir servisin yavaşlaması bütün zinciri yavaşlatıyor. İkincisi, dağıtık izleme ilk günden kurulmalı, sonradan eklemek çok daha zor. Üçüncüsü, servisler arası sözleşme bir yerde yazılı olmalı; Fastify'ın şemadan OpenAPI üretmesinin değerli olduğu yer tam olarak burası.
JS dışına çıkarsan
Bu yazının çoğu JavaScript ekosistemi üzerinden ilerledi ama giriş katmanı ve arayüz stratejisi dilden bağımsız.
PHP tarafında FrankenPHP ilginç bir noktada duruyor: Caddy'nin üstüne kurulu, yani uygulama sunucusu ile ters vekil aynı binary içinde geliyor ve otomatik HTTPS hazır. Laravel ile çalışıyorsan klasik PHP-FPM + Nginx ikilisine göre hareketli parça sayısı düşüyor. Octane da benzer şekilde süreç başlatma maliyetini ortadan kaldırıyor.
Go tarafında standart kütüphanenin net/http'si tek başına üretime yeterli; ek bir framework ihtiyacı çoğu serviste ortaya çıkmıyor. Tek binary çıkması dağıtımı da basitleştiriyor.
Dil seçimi zaten belliyse bu tartışmanın yarısı kapanıyor. Geriye önündeki katman ve arayüz stratejisi kalıyor, ki yukarıdaki kararlar orada aynen geçerli.
Senaryolara göre tercihler
Tek VPS, birkaç servis, ince arayüz. Caddy + Node + Fastify. Arayüzü aynı servisten ver. Bu kurulumun bakım maliyeti en düşük olanı ve büyük ihtimalle ihtiyacın olan tek şey bu.
Docker Compose, on üstü servis, sık deploy. Traefik + Node. Framework tercihi serviste ne yaptığına göre değişir; sözleşme ağırlıklıysa Fastify, ince ve çok sayıda uç nokta varsa Hono.
Coğrafi olarak dağınık kullanıcı, küçük ve hızlı API. Hono + Workers. Ama ağır işi (rapor, dosya, uzun süren hesap) buradan ayır, ayrı bir servise koy.
Dağıtım hedefi belirsiz, arayüz ve API birlikte. Nitro. Sonradan taşımak istediğinde preset değiştirmek, mimariyi değiştirmekten ucuz.
Çok ekipli, dışarıya API satan yapı. Gateway katmanı (Kong ya da APISIX) burada gerçekten karşılığını veriyor. Daha küçük ölçekte aynı şeyi kurmak, çözdüğünden fazla sorun üretir.
Bu listede tek bir kalıcı doğru yok ama tekrar eden bir hata var: aracı ihtiyaçtan önce seçmek. Çoğu ekip Kubernetes'e, gateway'e ve mikroservise, ölçeği o noktaya gelmeden çok önce geçiyor ve ölçeklenebilirlik adına ödediği bedeli sabit bakım yükü olarak taşıyor. Ters yönde hata yapmak, yani basit kurulumla başlayıp gerektiğinde bölmek, çok daha ucuza geri alınabiliyor.