Reverse Proxy, API Gateway ve YARP: Ne Zaman YARP, Ne Zaman Nginx veya Managed Gateway?
YARP, gateway'i kendi ASP.NET Core uygulaman olarak yazmanı sağlayan bir reverse proxy library'sidir; gateway'de koda dayalı mantık gerekiyorsa seçilir, statik kurallar için Nginx veya ingress, abonelik ve developer portalı için managed gateway daha uygundur. Bookstore.Gateway'i LoadFromConfig ve MapReverseProxy ile kurup route, cluster ve destination modelini, gateway 404'ü ile backend 404'ünü ve 502'yi gerçek loglarla ayırıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ve Yarp.ReverseProxy 2.3 ile derlenip denenmiştir.
Önce Tahmin Et
Önünde üç uygulama var: localhost:5301'de çalışan bir Catalog service'i, localhost:5302'de çalışan bir Ordering service'i ve localhost:5300'de ikisinin önünde duran, YARP (Yet Another Reverse Proxy; Microsoft'un .NET için yazdığı reverse proxy library'si) ile kurulmuş bir gateway. Gateway'de yalnız iki route var: /api/catalog/{**rest} ve /api/orders/{**rest}. Üç istek atıyorsun:
curl -i http://localhost:5300/api/catalog/books/0000
curl -i http://localhost:5300/api/inventory
# Ordering service kapalıyken:
curl -i http://localhost:5300/api/orders
Üçünün de status code'unu tahmin et. Daha önemlisi: hangisinin cevabını gateway kendisi veriyor, hangisi backend'den geliyor? İlk ikisi aynı kodu dönse bile aralarında büyük bir fark var ve gateway loglarına bakmadan bunu anlayamazsın. Cevapları yazının ortasında gerçek çıktılarla göreceğiz.
Reverse Proxy Ne Yapar, API Gateway Neyi Ekler?
Reverse proxy, istemcinin gerçek sunucular yerine konuştuğu ara sunucudur. İstemci tek bir adres bilir; proxy isteği arkadaki doğru uygulamaya iletir ve cevabı geri taşır. Web Server'lar ve Hosting serisinin ilk yazısında bir HTTP isteğinin uygulamaya nasıl ulaştığını anlatırken bu katmanı görmüştük. Burada önemli olan, reverse proxy'nin üç işi:
- TLS (Transport Layer Security; HTTPS'in altındaki şifreleme protokolü) termination: HTTPS bağlantısı proxy'de sonlanır, certificate'lar tek yerde yönetilir, arkadaki service'ler şifre çözmekle uğraşmaz. TLS ayarlarını her service'te ayrı ayrı yapmak zorunda kalmazsın.
- Connection abstraction: istemciyle proxy arasındaki bağlantı ile proxy'yle backend arasındaki bağlantı birbirinden bağımsızdır. İstemci HTTP/1.1 ile kısa bağlantılar açarken proxy arkada havuzlanmış, uzun ömürlü bağlantılar kullanabilir; HTTP sürümünü yükseltip düşürebilir.
- URL space kontrolü: dışarıya gösterdiğin URL'ler ile service'lerin içerideki adresleri ayrılır. Catalog service'ini başka bir makineye taşıdığında ya da ikiye böldüğünde istemcinin bildiği
/api/catalog/booksdeğişmez.
API gateway bu temelin üstüne API'ye özgü işler ekler: kimlik doğrulamayı ilk kapıda yapmak, rate limiting, response cache, header ve path dönüşümleri, birden fazla service'in cevabını birleştiren aggregation endpoint'leri, versiyonlama. Yani her gateway bir reverse proxy'dir, ama her reverse proxy bir gateway değildir. Kavramın kendisini, BFF (Backend for Frontend; belirli bir istemci için yazılmış ara backend) ayrımını ve gateway'in hangi noktada "her şeyi bilen" bir darboğaza dönüştüğünü Microservices serisinin API Gateway ve Backend for Frontend yazısında konuştuk. Bu seri o kavramın .NET'te somut olarak nasıl kurulduğunu anlatıyor.
YARP Bir Ürün Değil, Bir Library
YARP'ı Nginx gibi indirip config dosyası yazdığın hazır bir sunucu olarak düşünme. YARP bir NuGet paketi: Yarp.ReverseProxy. Gateway'in, senin yazdığın sıradan bir ASP.NET Core uygulaması oluyor. Kestrel üstünde çalışıyor, aynı DI (Dependency Injection; bağımlılıkların dışarıdan verilmesi) container'ını, aynı logging altyapısını, aynı authentication, authorization, rate limiting ve output cache middleware'lerini kullanıyor. Proxy davranışı da ASP.NET Core routing'ine eklenen endpoint'lerden ibaret.
Bu seçimin iki sonucu var. İyi tarafı: gateway'e eklemek istediğin her davranışı C# ile yazarsın. Kendi load balancing policy'n, veritabanından okunan route tablosu, isteğe göre hedef seçen bir middleware, hepsi tanıdık bir dil ve debugger ile yapılır. Diğer tarafı: o uygulama artık senin uygulaman. Patch'ini, deploy'unu, ölçeklemesini, izlemesini sen yaparsın. Hazır bir ürünün sunduğu yönetim paneli, developer portalı ya da API anahtarı yönetimi gelmez.
Yazıyı yazdığım gün (14 Kasım 2026) NuGet'teki son kararlı sürüm 27 Şubat 2025'te yayımlanan 2.3.0. Release notlarına göre bu sürüm .NET 8 ve sonrasını destekliyor; repo bu sürümle birlikte microsoft organizasyonundan dotnet organizasyonuna taşındı ve dokümantasyon Microsoft Learn'e geçti. Serinin bütün örnekleri .NET 10 üzerinde 2.3.0 ile çalışıyor.
Karar: YARP mı, Nginx mi, Envoy mi, Managed Gateway mi?
Kod yazmadan önce asıl soruyu soralım: buna gerçekten ihtiyacın var mı? Aşağıdaki tablo, sık karşılaştığım dört seçeneği hangi durumda seçtiğimi özetliyor:
| Seçenek | En çok işe yaradığı durum | Genişletme | Dikkat |
|---|---|---|---|
| YARP | .NET ekibi, gateway'de koda dayalı mantık (dinamik route, özel load balancing, aggregation, BFF) | C#, ASP.NET Core middleware'leri, kendi config provider'ın | İşletmesi sende; hazır portal ve API anahtarı yönetimi yok |
| Nginx | Statik kurallar, TLS termination, statik dosya, tek ya da birkaç upstream | Config dosyası; ileri mantık için module veya script | Kural karmaşıklaştıkça config okunmaz hâle gelir |
| Envoy, Kubernetes Gateway API | Kubernetes üstünde, platform ekibinin yönettiği ortak edge | Dinamik config API'si, Gateway API'nin HTTPRoute kaynakları | Uygulama ekibi için öğrenme maliyeti; .NET koduyla genişletilmez |
| Managed API gateway (Azure API Management, Amazon API Gateway) | Dışarıya açılan, abonelik, kota ve developer portalı isteyen API'ler | Sağlayıcının policy dili ve eklentileri | Maliyet, sağlayıcıya bağımlılık, ek bir ağ hop'u |
Nginx'i ASP.NET Core önünde nasıl kurduğumuzu Web Server'lar ve Hosting serisinin Nginx yazısında, Kubernetes'teki Ingress ve Gateway API tarafını da Kubernetes serisinin dış trafik yazısında anlattık. Bu seçenekler birbirini dışlamıyor. Sık gördüğüm bir yapı şöyle: önde bulut sağlayıcısının load balancer'ı ya da bir ingress TLS'i sonlandırıyor, onun arkasında YARP ile yazılmış gateway uygulamaya özgü routing ve kimlik kontrolünü yapıyor.
YARP'ın gereksiz olduğu durumlar da net. Tek bir API'n varsa ve önünde zaten IIS (Internet Information Services; Microsoft'un Windows için web sunucusu), Nginx ya da bir ingress duruyorsa, araya bir .NET süreci daha koymak yalnızca bir hop ve bir deploy birimi ekler. Kuralların statikse (şu path şu adrese gitsin, o kadar) ve hiç kod yazmayacaksan, Nginx veya ingress aynı işi daha az parça ile yapar. YARP'ın asıl değeri, gateway'de karar verirken uygulama bilgisine ve koda ihtiyaç duyduğunda ortaya çıkıyor.
İlk Gateway: Bookstore.Gateway
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) bu seride Catalog ve Ordering module'lerini ayrı süreçler olarak çalıştırıyor: Bookstore.Catalog.Api ve Bookstore.Ordering.Api. Gerçek bölme işini ilerideki .NET ile Mikroservis Uygulaması serisinde yapacağız; burada iki küçük Minimal API yeterli, çünkü konumuz önlerindeki kapı. Klasör yapısı şu:
Bookstore.slnx
Directory.Build.props // net10.0, Nullable, ImplicitUsings
Directory.Packages.props // paket sürümleri tek yerde
src/Bookstore.Gateway/ // :5300
src/Bookstore.Catalog.Api/ // :5301
src/Bookstore.Ordering.Api/ // :5302
Paket sürümlerini central package management ile tek dosyada tutuyoruz. Gateway projesinin csproj'u tek bir referanstan ibaret:
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
</Project>
<!-- src/Bookstore.Gateway/Bookstore.Gateway.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" />
</ItemGroup>
</Project>
Gateway'in Program.cs'i, YARP'ın en kısa hâli:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapGet("/gateway/ping", () => "pong");
app.MapReverseProxy();
app.Run();
AddReverseProxy() YARP'ın service'lerini DI'a ekliyor, LoadFromConfig route ve cluster tanımlarını configuration'daki ReverseProxy bölümünden okuyor, MapReverseProxy() da bu tanımları ASP.NET Core routing'ine endpoint olarak ekliyor. /gateway/ping satırını bilerek koydum: YARP route'ları senin kendi endpoint'lerinle aynı routing tablosunda yan yana yaşıyor. 05. yazıdaki BFF bu özelliğin üstüne kurulacak.
Asıl tanım appsettings.json'da:
"ReverseProxy": {
"Routes": {
"catalog": {
"ClusterId": "catalog",
"Match": {
"Path": "/api/catalog/{**rest}"
}
},
"orders": {
"ClusterId": "ordering",
"Match": {
"Path": "/api/orders/{**rest}"
}
}
},
"Clusters": {
"catalog": {
"Destinations": {
"catalog-1": { "Address": "http://localhost:5301/" }
}
},
"ordering": {
"Destinations": {
"ordering-1": { "Address": "http://localhost:5302/" }
}
}
}
}
Catalog tarafı, bilerek sade tutulmuş bir Minimal API. Dört kitap bellekte duruyor ve endpoint'ler dışarıdaki URL ile aynı path'i dinliyor:
Book[] books =
[
new("9780441172719", "Dune", "Frank Herbert", 420.00m),
new("9780141439587", "Emma", "Jane Austen", 185.50m),
new("9780547928227", "The Hobbit", "J. R. R. Tolkien", 310.00m),
new("9780141439549", "Middlemarch", "George Eliot", 265.00m),
];
app.MapGet("/api/catalog/books", () => books);
app.MapGet("/api/catalog/books/{isbn}", (string isbn) =>
books.FirstOrDefault(b => b.Isbn == isbn) is { } book
? Results.Ok(book)
: Results.NotFound());
Path'lerin aynı olması bir tesadüf değil. YARP varsayılan olarak gelen path'i değiştirmeden iletir: gateway'e gelen /api/catalog/books, Catalog'a da /api/catalog/books olarak gider. Dış URL'yi içeriden ayırmak transform'ların işi ve bir sonraki yazının konusu. Üç projeyi ayrı terminallerde çalıştırıp gateway'e soralım (Windows PowerShell 5.1 kullanıyorsan curl yerine curl.exe yaz):
dotnet run --project src/Bookstore.Catalog.Api
dotnet run --project src/Bookstore.Ordering.Api
dotnet run --project src/Bookstore.Gateway
curl -i http://localhost:5300/api/orders/1001
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 10:25:42 GMT
Server: Kestrel
Transfer-Encoding: chunked
{"id":1001,"customerEmail":"ada@example.com","total":605.50,"status":"Shipped"}
Route, Cluster, Destination
YARP'ın bütün modeli bu üç kavrama dayanıyor ve serinin geri kalanında her şeyi bunların üstüne ekleyeceğiz:
- Route: "hangi istekler?" sorusunun cevabı.
Matchbölümünde path, host, HTTP method, header ve query parametresi koşulları tanımlanır. Route ayrıca birClusterIdve route'a özgü policy'leri (authorization, rate limiting, timeout, CORS (Cross-Origin Resource Sharing; tarayıcının başka origin'e yaptığı istekleri yöneten kurallar), output cache) taşır. - Cluster: "nereye ve nasıl?" sorusunun cevabı. Bir veya daha fazla destination'ı, load balancing policy'sini, health check ayarlarını, session affinity'yi ve backend'e giden
HttpClient'ın ayarlarını içerir. - Destination: cluster içindeki tek bir adres. Aynı service'in üç instance'ı, bir cluster'da üç destination olur.
Route'lar ile cluster'lar arasındaki ilişki çoktan teke: birçok route aynı cluster'a gidebilir. Örneğin ileride /api/v1/books ve /api/catalog/** aynı Catalog cluster'ını kullanacak. Route'ların hangi sırada denendiğini ise ASP.NET Core routing'inin öncelik kuralları belirliyor: daha spesifik path önce gelir, gerektiğinde route'a Order verilir ve küçük değer önceliklidir. Bu sıralamanın nasıl ters tepebileceğini 02'de göreceğiz.
İsteğin YARP Pipeline'ından Geçişi

Bir istek gateway'e geldiğinde önce normal ASP.NET Core middleware'lerinden geçer, sonra routing bir YARP route'u eşleştirir. Eşleşme olduğunda YARP'ın kendi küçük pipeline'ı çalışır. Parametresiz MapReverseProxy() bu pipeline'a sırasıyla session affinity, load balancing ve passive health check adımlarını koyar; en sonda isteği gerçekten gönderen forwarder vardır. YARP'ın kaynak kodunda bu varsayılan sıra açıkça yazıyor. Aynı pipeline'ı kendin kurup araya bir adım ekleyebilirsin:
using Yarp.ReverseProxy.Model;
app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.Use(async (context, next) =>
{
IReverseProxyFeature proxy = context.GetReverseProxyFeature();
ILogger logger = context.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Bookstore.Gateway");
logger.LogInformation(
"Route {RouteId} -> cluster {ClusterId}, {Count} available destination(s)",
proxy.Route.Config.RouteId,
proxy.Cluster.Config.ClusterId,
proxy.AvailableDestinations.Count);
await next();
logger.LogInformation(
"Forwarded to {DestinationId}, status {StatusCode}",
proxy.ProxiedDestination?.DestinationId,
context.Response.StatusCode);
});
// overload'u kullanınca varsayılan adımları kendimiz ekliyoruz
proxyPipeline.UseSessionAffinity();
proxyPipeline.UseLoadBalancing();
proxyPipeline.UsePassiveHealthChecks();
});
IReverseProxyFeature, YARP'ın o istek için seçtiği route'u, cluster'ı, kullanılabilir destination listesini ve sonunda isteğin gönderildiği destination'ı taşıyor. Overload'u kullandığında varsayılan üç adım otomatik eklenmez; onları unutursan load balancing ve passive health check sessizce devre dışı kalır. Şimdi "Önce Tahmin Et"teki ilk isteği atalım ve gateway'in logunu okuyalım:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5300/api/catalog/books/0000
404
info: Bookstore.Gateway[0]
Route catalog -> cluster catalog, 1 available destination(s)
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[9]
Proxying to http://localhost:5301/api/catalog/books/0000 HTTP/2 RequestVersionOrLower
info: Yarp.ReverseProxy.Forwarder.HttpForwarder[56]
Received HTTP/1.1 response 404.
info: Bookstore.Gateway[0]
Forwarded to catalog-1, status 404
Bu 404 backend'den geliyor: route eşleşti, istek Catalog'a gitti, Catalog o ISBN'i (kitabın uluslararası standart numarası) bulamadı. Logdaki HTTP/2 RequestVersionOrLower ifadesine de dikkat et. YARP backend'e varsayılan olarak HTTP/2 ile gitmeye çalışıyor, ama düz HTTP üzerinde HTTP/2 anlaşması TLS gerektirdiği için bağlantı HTTP/1.1'e düşüyor. Bu ayarı cluster'ın HttpRequest bölümünden değiştirmeyi 03'te konuşacağız.
İkinci istek, hiçbir route'a uymayan /api/inventory:
curl -i http://localhost:5300/api/inventory
HTTP/1.1 404 Not Found
Content-Length: 0
Date: Sat, 14 Nov 2026 10:25:41 GMT
Server: Kestrel
Status code yine 404, ama bu sefer gateway logunda ne route satırı ne de Proxying to satırı var. Hiçbir endpoint eşleşmediği için cevabı ASP.NET Core'un kendisi verdi ve hiçbir backend'e gidilmedi. Üretimde "404 alıyoruz" şikâyeti geldiğinde ilk bakacağın şey bu: istek gateway'de mi düştü, yoksa backend mi bulamadı?
Üçüncü istek için Ordering service'ini kapattım:
curl -i http://localhost:5300/api/orders
HTTP/1.1 502 Bad Gateway
Content-Length: 0
Date: Sat, 14 Nov 2026 10:25:49 GMT
Server: Kestrel
warn: Yarp.ReverseProxy.Forwarder.HttpForwarder[48]
Request: An error was encountered before receiving a response.
System.Net.Http.HttpRequestException: Connection refused (localhost:5302)
Route eşleşti, destination seçildi, ama bağlantı kurulamadı. YARP bunu 502 Bad Gateway ile istemciye bildiriyor ve logda hatanın türünü (Request, yani cevap gelmeden önce oluşan hata) yazıyor. Body boş; istemciye ne göstereceğine sen karar vereceksin.
Üretimde Dikkat
Gateway, arkasındaki bütün service'lerin önünde duran tek kapı olduğu için onun sorunları herkesin sorunu oluyor. İlk günden üç şeyi ayarla. Birincisi, gateway'i tek instance çalıştırma; en az iki instance ve önlerinde bir load balancer olsun. YARP'ın kendisi stateless, ama ileride ekleyeceğimiz rate limit sayaçları ve output cache instance başına tutuluyor; bunu 07'de konuşacağız. İkincisi, gateway loglarında yukarıda gördüğün Proxying to ve Received ... response satırları her istek için Information seviyesinde yazılıyor. Yoğun trafikte bu, log sisteminin en büyük gürültü kaynağı olur; Yarp kategorisini Warning'e çekip istek başına bilgiyi access log ya da metric'lerden almak daha doğru. Üçüncüsü, gateway'in kendi health endpoint'i olsun ve backend'lerin sağlığından ayrı ölçülsün; gateway ayakta ama arkadaki her şey düşükse load balancer'ın bunu bilmesi gerekmez, senin bilmen gerekir.
Bir de ölçüm meselesi var. Gateway her isteğe bir ağ hop'u ve biraz işlem süresi ekliyor. Bunu tahminle değil ölçerek bilmek gerek; 07'de load test ile gateway'li ve gateway'siz gecikmeyi karşılaştıracağız.
Sık Düşülen Tuzaklar
İlk tuzak, MapReverseProxy'nin overload'unu kullanıp varsayılan adımları eklemeyi unutmak. Pipeline derleniyor, istekler gidiyor, ama birden fazla destination eklediğinde load balancing'in çalışmadığını ancak yük altında fark ediyorsun.
İkincisi, path'in aynen iletildiğini unutmak. Gateway'de /api/catalog/books diye bir route tanımlayıp Catalog'un /books dinlediğini varsayarsan her istek 404 döner ve log sana sadece backend'in 404 verdiğini söyler. Gateway'in backend'e tam olarak hangi URL'yi gönderdiğini Proxying to satırında görürsün; ilk bakacağın yer orası.
Üçüncüsü, gateway'i business logic koyulacak yer sanmak. Sipariş tutarı kontrolü, stok kuralı, kampanya hesabı gateway'e girdiğinde ortada bir "god gateway" oluşur: her service değişikliği gateway deploy'u gerektirir. Gateway routing, kimlik ve trafik kuralı gibi cross-cutting işleri yapar; domain kararı service'lerde kalır.
Dördüncüsü, cluster adresini localhost ile bırakıp gateway'i container'a koymak. Container içinde localhost container'ın kendisidir; adresleri environment'a göre değiştirmeyi 02'de config filter ile, dinamik bulmayı 03'te service discovery ile göreceğiz.
Küçük Bir Deneme
★☆☆ /gateway/ping endpoint'ini silmeden ReverseProxy:Routes altına "Path": "/gateway/{**rest}" eşleşmesi olan ve Ordering cluster'ına giden bir route ekle. curl http://localhost:5300/gateway/ping hâlâ pong mu dönüyor? Cevabı ASP.NET Core routing'inin öncelik kurallarıyla açıkla.
★★☆ Ordering service'i kapalıyken bir istek at ve logdaki forwarder hata türünü not et. Sonra Ordering'i başlatıp hemen ardından tekrar dene. Gateway'i yeniden başlatman gerekti mi? Neden gerekmediğini bağlantı havuzu açısından düşün.
★★★ Pipeline'a eklediğimiz middleware'i, cevap 5xx olduğunda Warning seviyesinde, diğer durumlarda Debug seviyesinde log yazacak şekilde değiştir; Yarp kategorisini Warning'e çek. Böylece normal trafikte gateway'in logu neredeyse boş kalsın, hatalar ise route, cluster ve destination bilgisiyle görünsün.
Şimdilik route'larımız yalnızca path'e bakıyor ve dış URL ile iç URL aynı. Bir sonraki yazıda route match'lerini host, method, header ve query ile zenginleştireceğiz; transform'larla /api/v1/books isteğini Catalog'a /books olarak göndereceğiz ve route tablosunu uygulamayı yeniden başlatmadan değiştireceğiz.