In-Depth Guide · 8 Sections · Microservices Edge
API Gateway In Depth
Why you need a gateway, Ocelot basics (MicroservicesOcelotDemo), YARP reverse proxy, JWT auth forwarding, BFF aggregation, rate limiting and production deployment. Plain English, copy-paste .NET 9 code.
Ocelot
YARP
JWT & BFF
Rate Limiting
Prerequisite: ASP.NET Core APIs — Minimal API or REST API tutorial.
For resilience behind the gateway see Redis & Polly; for cross-service messaging see MassTransit.
In microservices, clients would otherwise call dozens of services directly — juggling URLs, auth, retries and CORS. An API Gateway sits at the edge: single entry point, routes inward, handles cross-cutting concerns once.
1.1
What the gateway owns
// Client sees ONE base URL:
// https://api.myshop.com
// Gateway responsibilities:
// • Routing /orders/* → OrderService
// /products/* → CatalogService
// • SSL termination (HTTPS at edge)
// • Authentication (validate JWT once)
// • Rate limiting (protect backends)
// • Request aggregation (BFF — one call, many services)
// • Header injection (correlation ID, tenant ID)
// Backends stay internal — not exposed to the internet
// OrderService: http://order-svc:8080
// CatalogService: http://catalog-svc:8080
1.2
Ocelot vs YARP — when to pick
// OCELOT
// • JSON-configured routes (ocelot.json)
// • Built-in aggregation, QoS, caching, auth providers
// • Mature microservices feature set out of the box
// • Good for: config-driven gateway, quick microservices demos
// YARP (Yet Another Reverse Proxy)
// • Microsoft-maintained, high-performance reverse proxy
// • Code-first or appsettings.json routes + transforms
// • Extensible middleware — feels like ASP.NET Core
// • Good for: custom logic, K8s ingress replacement, BFF apps
// Both are valid — many teams migrate Ocelot → YARP for performance
// Azure API Management / AWS API Gateway = managed SaaS alternative
2.1
Setup & ocelot.json routes
// NuGet: Ocelot
// Program.cs
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);
// ...
await app.UseOcelot(); // must be last in pipeline
// ocelot.json
{
"Routes": [
{
"DownstreamPathTemplate": "/api/products/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [{ "Host": "catalog-service", "Port": 8080 }],
"UpstreamPathTemplate": "/catalog/{everything}",
"UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ],
"AuthenticationOptions": { "AuthenticationProviderKey": "Bearer" },
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 30,
"TimeoutValue": 5000
}
}
],
"GlobalConfiguration": { "BaseUrl": "https://gateway.myshop.com" }
}
2.2
Aggregation — multi-service response
// Ocelot aggregates parallel downstream calls into one response
{
"Routes": [
{
"Key": "product",
"DownstreamPathTemplate": "/api/products/{productId}",
"DownstreamHostAndPorts": [{ "Host": "catalog-service", "Port": 8080 }],
"UpstreamPathTemplate": "/product-detail/{productId}",
"UpstreamHttpMethod": [ "Get" ]
},
{
"Key": "reviews",
"DownstreamPathTemplate": "/api/reviews/{productId}",
"DownstreamHostAndPorts": [{ "Host": "review-service", "Port": 8080 }],
"UpstreamPathTemplate": "/product-detail/{productId}",
"UpstreamHttpMethod": [ "Get" ]
}
],
"Aggregates": [
{
"RouteKeys": [ "product", "reviews" ],
"UpstreamPathTemplate": "/product-detail/{productId}",
"Aggregator": "ProductDetailAggregator"
}
]
}
YARP is a lightweight reverse proxy library — configure routes in code or JSON, extend with standard ASP.NET Core middleware.
3.1
appsettings.json routes
// NuGet: Yarp.ReverseProxy
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
app.MapReverseProxy();
// appsettings.json
"ReverseProxy": {
"Routes": {
"orders-route": {
"ClusterId": "orders-cluster",
"Match": { "Path": "/api/orders/{**catch-all}" },
"Transforms": [
{ "PathRemovePrefix": "/api/orders" },
{ "RequestHeader": "X-Gateway", "Set": "YARP" }
]
},
"catalog-route": {
"ClusterId": "catalog-cluster",
"Match": { "Path": "/api/catalog/{**catch-all}" }
}
},
"Clusters": {
"orders-cluster": {
"Destinations": {
"d1": { "Address": "http://order-service:8080/" }
},
"HealthCheck": { "Active": { "Enabled": true, "Interval": "00:00:30", "Path": "/health" } }
},
"catalog-cluster": {
"Destinations": {
"d1": { "Address": "http://catalog-service:8080/" }
}
}
}
}
3.2
Code-first routes & load balancing
builder.Services.AddReverseProxy()
.LoadFromMemory(routes, clusters);
var routes = new[]
{
new RouteConfig
{
RouteId = "orders",
ClusterId = "orders",
Match = new RouteMatch { Path = "/api/orders/{**remainder}" }
}
};
var clusters = new[]
{
new ClusterConfig
{
ClusterId = "orders",
LoadBalancingPolicy = "RoundRobin",
Destinations = new Dictionary<string, DestinationConfig>
{
["a"] = new() { Address = "http://order-1:8080/" },
["b"] = new() { Address = "http://order-2:8080/" }
}
}
};
Validate the JWT once at the gateway, then forward the token (or transformed claims) to downstream services. Backends trust the internal network or re-validate if zero-trust.
4.1
JWT validation at gateway
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy().RequireAuthorization(); // all proxied routes need JWT
// Ocelot equivalent in ocelot.json:
// "AuthenticationOptions": { "AuthenticationProviderKey": "Bearer" }
4.2
Forward token & inject claims as headers
// YARP transform — pass user identity to downstream without re-parsing JWT
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(builderContext =>
{
builderContext.AddRequestTransform(async transformContext =>
{
var user = transformContext.HttpContext.User;
if (user.Identity?.IsAuthenticated == true)
{
transformContext.ProxyRequest.Headers.TryAddWithoutValidation(
"X-User-Id", user.FindFirst("sub")?.Value ?? "");
transformContext.ProxyRequest.Headers.TryAddWithoutValidation(
"X-User-Roles", string.Join(",", user.FindAll("role").Select(c => c.Value)));
}
// Authorization header forwarded automatically by YARP
});
});
// Downstream service reads X-User-Id — or re-validates JWT for zero-trust
Watch Out — trust boundaries
Headers like X-User-Id are spoofable if backends are reachable without the gateway. Only accept them on internal networks, or always re-validate JWT downstream in zero-trust setups.
A Backend-for-Frontend (BFF) gateway endpoint calls multiple services, shapes the response for a specific client (mobile vs web), and hides internal complexity.
5.1
BFF endpoint with IHttpClientFactory
// Gateway project — NOT pure proxy; owns aggregation logic
builder.Services.AddHttpClient("catalog", c => c.BaseAddress = new Uri("http://catalog:8080/"));
builder.Services.AddHttpClient("reviews", c => c.BaseAddress = new Uri("http://reviews:8080/"));
app.MapGet("/mobile/product/{id:int}", async (
int id,
IHttpClientFactory factory,
CancellationToken ct) =>
{
var catalog = factory.CreateClient("catalog");
var reviews = factory.CreateClient("reviews");
var productTask = catalog.GetFromJsonAsync<ProductDto>($"/api/products/{id}", ct);
var reviewsTask = reviews.GetFromJsonAsync<ReviewSummary>($"/api/reviews/{id}", ct);
await Task.WhenAll(productTask, reviewsTask);
return Results.Ok(new MobileProductDetail(
productTask.Result!,
reviewsTask.Result!,
// Mobile-specific shaping — smaller payload
RelatedSkus: productTask.Result!.Related.Take(3).ToList()));
});
5.2
When BFF vs pure reverse proxy
// Pure reverse proxy (YARP/Ocelot route only):
// • Simple pass-through /api/orders → order-service
// • No response shaping
// • Minimal gateway CPU
// BFF endpoint (custom code in gateway):
// • Aggregate 3+ services into one mobile-friendly DTO
// • Client-specific auth (cookie for web, JWT for mobile)
// • GraphQL gateway is an alternative BFF style
// Rule: start with reverse proxy; add BFF endpoints when
// clients suffer from chatty multi-call workflows
Protect backends from abuse by enforcing limits at the gateway — per IP, per user, or per API key — before traffic reaches microservices.
6.1
ASP.NET Core rate limiter
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("api", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 100;
opt.QueueLimit = 0;
});
options.AddPolicy("per-user", httpContext =>
RateLimitPartition.GetTokenBucketLimiter(
partitionKey: httpContext.User.FindFirst("sub")?.Value
?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "anon",
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 50,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 10,
AutoReplenishment = true
}));
});
app.UseRateLimiter();
app.MapReverseProxy().RequireRateLimiting("api");
6.2
Redis-backed limits (multi-instance gateway)
// In-memory limiter is per gateway instance — use Redis for cluster-wide counts
// NuGet: AspNetCoreRateLimit or custom middleware with StackExchange.Redis
public sealed class RedisRateLimitMiddleware(RequestDelegate next, IConnectionMultiplexer mux)
{
public async Task InvokeAsync(HttpContext ctx)
{
var key = $"rl:{ctx.Connection.RemoteIpAddress}";
var db = mux.GetDatabase();
var count = await db.StringIncrementAsync(key);
if (count == 1)
await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
if (count > 100)
{
ctx.Response.StatusCode = 429;
ctx.Response.Headers.RetryAfter = "60";
return;
}
await next(ctx);
}
}
7.1
Path transforms — strip prefix
// External: GET /api/v1/orders/42
// Internal: GET /orders/42 (strip /api/v1)
"Transforms": [
{ "PathPattern": "/orders/{**remainder}" },
{ "RequestHeaderOriginalHost": "true" },
{ "X-Forwarded": "Set" }
]
// Ocelot equivalent:
// "UpstreamPathTemplate": "/api/v1/orders/{id}"
// "DownstreamPathTemplate": "/orders/{id}"
// Always forward X-Forwarded-For / X-Forwarded-Proto
// so backends build correct URLs and audit client IPs
7.2
Health-based routing & circuit breaking
// YARP active health checks — remove unhealthy destinations
"HealthCheck": {
"Active": {
"Enabled": true,
"Interval": "00:00:15",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health/ready"
},
"Passive": {
"Enabled": true,
"Policy": "TransportFailureRate",
"ReactivationPeriod": "00:01:00"
}
}
// Combine with Polly on BFF HttpClient calls (section 5)
// Gateway health: /health/live + /health/ready (see Redis tutorial)
- TLS termination at gateway or ingress (cert-manager / Azure Front Door).
- JWT validation at edge; define trust model for downstream headers.
- Rate limits per client + global circuit breaker on flaky clusters.
- Observability — correlation ID injected at gateway; trace spans per hop.
- Config reload — YARP/Ocelot support hot reload; use for route changes.
- Don't put business logic in gateway — BFF aggregation only, not domain rules.
- Horizontal scale — gateway is stateless; Redis for shared rate-limit counters.
// Kubernetes: YARP gateway as Deployment + Service
// Ingress NGINX or Azure Application Gateway → YARP pod → internal services
// docker-compose (dev):
// gateway:5000 → catalog:8080, orders:8080, reviews:8080
// Logging at gateway (every request):
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next();
log.LogInformation("{Method} {Path} → {Status} in {Ms}ms",
ctx.Request.Method, ctx.Request.Path, ctx.Response.StatusCode, sw.ElapsedMilliseconds);
});
8.2
Ocelot vs YARP — production picker
// Choose OCELOT when:
// • JSON-driven routes, team prefers config over code
// • Need built-in aggregation with minimal C#
// • Existing MicroservicesOcelotDemo-style setup
// Choose YARP when:
// • Performance and Microsoft long-term support matter
// • Heavy custom middleware / BFF logic in gateway process
// • Replacing nginx/Envoy with .NET-native proxy
// Managed alternative:
// • Azure API Management — policies, developer portal, SKU cost
// • AWS API Gateway — Lambda integration, usage plans
// Demo repo: github.com/stevsharp/MicroservicesOcelotDemo
Mastery Move — where to go next
1. MicroservicesOcelotDemo — clone and run the Ocelot gateway with real microservices.
2. Redis & Polly — resilience and observability for BFF HttpClient calls behind the gateway.
3. MassTransit — async communication between services the gateway routes to.