Learnixo
Back to blog
AI Systemsintermediate

API Gateway Pattern — Routing, Auth, and Rate Limiting

Implement the API Gateway pattern for microservices: YARP as a .NET reverse proxy, request routing, centralized authentication, rate limiting, request aggregation, and when to use BFF vs API Gateway.

Asma Hafeez KhanMay 16, 20264 min read
MicroservicesAPI GatewayYARPASP.NET Core.NETArchitecture
Share:𝕏

What the API Gateway Does

API Gateway: the single entry point for all client requests to your microservices.

Responsibilities:
  ✓ Request routing: /api/patients → PatientService, /api/prescriptions → PrescriptionService
  ✓ Authentication: validate JWT once at the gateway, pass identity downstream
  ✓ Rate limiting: protect services from traffic spikes and abuse
  ✓ SSL termination: TLS at the gateway, plain HTTP internally
  ✓ Request aggregation: combine results from multiple services for one client call
  ✓ Load balancing: distribute requests across service instances

What it does NOT replace:
  ✗ Authorization (each service still validates what the user can access)
  ✗ Business logic (the gateway is infrastructure, not application logic)

YARP — .NET Reverse Proxy

C#
// NuGet: Yarp.ReverseProxy

// Program.cs
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

app.MapReverseProxy();

// appsettings.json
{
  "ReverseProxy": {
    "Routes": {
      "patient-route": {
        "ClusterId": "patient-cluster",
        "Match": { "Path": "/api/patients/{**catch-all}" }
      },
      "prescription-route": {
        "ClusterId": "prescription-cluster",
        "Match": { "Path": "/api/prescriptions/{**catch-all}" }
      }
    },
    "Clusters": {
      "patient-cluster": {
        "Destinations": {
          "patient-1": { "Address": "http://patient-service:8080/" },
          "patient-2": { "Address": "http://patient-service-2:8080/" }
        }
      },
      "prescription-cluster": {
        "Destinations": {
          "prescription-1": { "Address": "http://prescription-service:8080/" }
        }
      }
    }
  }
}

Centralized JWT Validation

C#
// Validate JWT at the gateway — downstream services trust the gateway
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.systemforge.internal";
        options.Audience  = "systemforge-api";
    });

app.UseAuthentication();
app.UseAuthorization();

// Pass user claims downstream via headers
app.MapReverseProxy(proxyPipeline =>
{
    proxyPipeline.Use(async (context, next) =>
    {
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
            var roles  = context.User.FindAll(ClaimTypes.Role)
                .Select(c => c.Value)
                .ToList();

            context.Request.Headers["X-User-Id"]    = userId;
            context.Request.Headers["X-User-Roles"] = string.Join(",", roles);
        }
        await next();
    });
});

// Downstream services read X-User-Id from header — no need to validate JWT again
// They trust the gateway (mutual TLS or network policy enforces trust boundary)

Rate Limiting

C#
// NuGet: System.Threading.RateLimiting (built into .NET 7+)

builder.Services.AddRateLimiter(options =>
{
    // Global: 100 requests per minute per IP
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        context => RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit       = 100,
                Window            = TimeSpan.FromMinutes(1),
            }));

    // Per-endpoint: prescription creation is more expensive
    options.AddFixedWindowLimiter("prescriptions", opts =>
    {
        opts.PermitLimit  = 20;
        opts.Window       = TimeSpan.FromMinutes(1);
        opts.QueueLimit   = 5;
    });

    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});

app.UseRateLimiter();

BFF vs API Gateway

API Gateway (general):
  Single gateway for all clients (web, mobile, third-party).
  General-purpose routing and auth.
  Clients adapt to the API format.

Backend for Frontend (BFF):
  Dedicated gateway per client type.
  Web BFF: aggregates data specifically for the web dashboard
  Mobile BFF: aggregates data optimized for mobile payload size
  Third-party BFF: versioned, stable API for external partners

When to use BFF:
  → Different clients need very different data shapes
  → Mobile needs smaller payloads than web
  → Different teams own each frontend
  → Third-party API must be stable while internal services change

Most clinical systems: one API Gateway for external, BFF pattern for specific
  high-traffic client types (mobile app, ward dashboard).

Production issue I've seen: A team put JWT validation in every microservice individually. When the JWT secret rotated, they had to update 8 services simultaneously. Two services were missed — they continued accepting tokens signed with the old secret for 3 days after rotation. Centralizing JWT validation at the API gateway meant the rotation was a single configuration change in one place.


Key Takeaway

The API Gateway is the single entry point: routing, auth, rate limiting, SSL termination. YARP is the .NET-native reverse proxy for building API gateways. Validate JWT at the gateway and pass identity headers downstream — don't re-validate in every service. Use BFF when different clients need substantially different APIs. The gateway is infrastructure, not business logic — keep it thin.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.