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.

01

Why an API Gateway?

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
02

Ocelot Basics

Ocelot is a .NET API gateway configured via ocelot.json. See the working demo: MicroservicesOcelotDemo.

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"
    }
  ]
}
03

YARP Reverse Proxy

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/" }
        }
    }
};
04

Auth Forwarding — JWT

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.

05

Aggregation & BFF Pattern

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
06

Rate Limiting at the Edge

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);
    }
}
07

Routing, Transforms & Load Balancing

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)
08

Production — Deployment & Checklist

8.1

Production checklist

  • 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.