Skip to main content

APIGateway

45. API Gateway (YARP, Azure API Management)

Short Introduction

An API Gateway is a server that acts as an API front-end, receiving API requests, enforcing throttling and security policies, passing requests to the back-end service, and then passing the response back to the requester. YARP (Yet Another Reverse Proxy) is Microsoft's .NET-based reverse proxy toolkit, while Azure API Management provides a comprehensive managed API gateway solution.

Official Definition

An API Gateway is an API management tool that sits between a client and a collection of backend services. It acts as a reverse proxy to accept all API calls, aggregate the various services required to fulfill them, and return the appropriate result. YARP is a library to help create reverse proxy servers that are high-performance, production-ready, and highly customizable.

Setup/Usage with .NET 8+ Code

YARP Implementation:

# Install YARP package
dotnet add package Yarp.ReverseProxy

Program.cs with YARP:

// ApiGateway/Program.cs
using Yarp.ReverseProxy.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

// Add YARP
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
});

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy => policy.RequireClaim("role", "admin"));
options.AddPolicy("RequireUser", policy => policy.RequireAuthenticatedUser());
});

// Add rate limiting
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User?.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});

// Add CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

var app = builder.Build();

// Configure pipeline
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

// Map reverse proxy
app.MapReverseProxy();

app.Run();

YARP Configuration (appsettings.json):

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Yarp": "Warning"
}
},
"ReverseProxy": {
"Routes": {
"booking-route": {
"ClusterId": "booking-cluster",
"Match": {
"Path": "/api/bookings/{**catch-all}"
},
"Transforms": [
{
"PathPattern": "/api/bookings/{**catch-all}"
},
{
"RequestHeader": "X-Forwarded-Host",
"Set": "{OriginalHost}"
}
],
"AuthorizationPolicy": "RequireUser"
},
"room-route": {
"ClusterId": "room-cluster",
"Match": {
"Path": "/api/rooms/{**catch-all}"
},
"Transforms": [
{
"PathPattern": "/api/rooms/{**catch-all}"
}
]
},
"admin-route": {
"ClusterId": "admin-cluster",
"Match": {
"Path": "/api/admin/{**catch-all}"
},
"AuthorizationPolicy": "RequireAdmin",
"RateLimiterPolicy": "AdminPolicy"
}
},
"Clusters": {
"booking-cluster": {
"Destinations": {
"destination1": {
"Address": "http://booking-service:8080/"
}
},
"HealthCheck": {
"Active": {
"Enabled": "true",
"Interval": "00:00:30",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health"
}
},
"LoadBalancingPolicy": "RoundRobin"
},
"room-cluster": {
"Destinations": {
"destination1": {
"Address": "http://room-service:8080/"
},
"destination2": {
"Address": "http://room-service-2:8080/"
}
},
"HealthCheck": {
"Active": {
"Enabled": "true",
"Interval": "00:00:30",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health"
}
},
"LoadBalancingPolicy": "LeastRequests"
},
"admin-cluster": {
"Destinations": {
"destination1": {
"Address": "http://admin-service:8080/"
}
}
}
}
}
}

Custom YARP Middleware:

// Middleware/ApiGatewayMiddleware.cs
public class ApiGatewayMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiGatewayMiddleware> _logger;

public ApiGatewayMiddleware(RequestDelegate next, ILogger<ApiGatewayMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
// Add correlation ID
if (!context.Request.Headers.ContainsKey("X-Correlation-ID"))
{
context.Request.Headers.Add("X-Correlation-ID", Guid.NewGuid().ToString());
}

// Log request
_logger.LogInformation("Gateway request: {Method} {Path} from {RemoteIp}",
context.Request.Method,
context.Request.Path,
context.Connection.RemoteIpAddress);

// Add request timestamp
context.Request.Headers.Add("X-Gateway-Timestamp", DateTimeOffset.UtcNow.ToString("O"));

await _next(context);

// Log response
_logger.LogInformation("Gateway response: {StatusCode} for {Method} {Path}",
context.Response.StatusCode,
context.Request.Method,
context.Request.Path);
}
}

// Transforms/CustomTransforms.cs
public class AddCustomHeaderTransform : ITransformProvider
{
public void ValidateRoute(RouteConfig route, IList<Exception> errors)
{
// Validation logic if needed
}

public void ValidateCluster(ClusterConfig cluster, IList<Exception> errors)
{
// Validation logic if needed
}

public void Apply(TransformBuilderContext context)
{
// Add custom transforms
context.AddRequestTransform(transformContext =>
{
transformContext.ProxyRequest.Headers.Add("X-Gateway-Version", "1.0");
transformContext.ProxyRequest.Headers.Add("X-Request-ID", Guid.NewGuid().ToString());
return ValueTask.CompletedTask;
});

context.AddResponseTransform(transformContext =>
{
transformContext.HttpContext.Response.Headers.Add("X-Powered-By", "YARP Gateway");
return ValueTask.CompletedTask;
});
}
}

Azure API Management Integration:

// For comparison - Azure API Management would be configured via ARM template
// azuredeploy.json (excerpt)
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"apiManagementServiceName": {
"type": "string",
"defaultValue": "hotel-management-apim"
}
},
"resources": [
{
"type": "Microsoft.ApiManagement/service",
"apiVersion": "2021-08-01",
"name": "[parameters('apiManagementServiceName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Developer",
"capacity": 1
},
"properties": {
"publisherEmail": "admin@hotelmanagement.com",
"publisherName": "Hotel Management",
"notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com"
}
},
{
"type": "Microsoft.ApiManagement/service/apis",
"apiVersion": "2021-08-01",
"name": "[concat(parameters('apiManagementServiceName'), '/booking-api')]",
"dependsOn": [
"[resourceId('Microsoft.ApiManagement/service', parameters('apiManagementServiceName'))]"
],
"properties": {
"displayName": "Booking API",
"apiRevision": "1",
"subscriptionRequired": true,
"serviceUrl": "http://booking-service:8080",
"path": "bookings",
"protocols": ["https"]
}
}
]
}

Use Cases

  • Single Entry Point: Centralized access to multiple microservices
  • Cross-Cutting Concerns: Authentication, logging, rate limiting, CORS
  • API Versioning: Route requests to different API versions
  • Load Balancing: Distribute requests across multiple service instances
  • Protocol Translation: Convert between different protocols (HTTP, gRPC)
  • Request/Response Transformation: Modify requests and responses

When to Use vs When Not to Use

Use API Gateway when:

  • Running microservices architecture
  • Need centralized authentication and authorization
  • Require request routing and load balancing
  • Need to implement cross-cutting concerns
  • Want to hide internal service complexity from clients
  • Need API versioning and backwards compatibility

Consider alternatives when:

  • Simple monolithic applications
  • Direct service-to-service communication is preferred
  • Low latency requirements (additional network hop)
  • Limited operational complexity tolerance
  • Small number of services with simple routing needs

Market Alternatives & Pros/Cons

Alternatives:

  • Kong: Open-source API gateway with enterprise features
  • Istio: Service mesh with gateway capabilities
  • Ambassador: Kubernetes-native API gateway
  • AWS API Gateway: Managed API gateway service
  • Traefik: Modern reverse proxy and load balancer
  • Nginx Plus: Commercial version with API gateway features

Pros:

  • Centralized cross-cutting concerns
  • Single entry point for clients
  • Built-in load balancing and health checks
  • Request/response transformation capabilities
  • Integration with .NET ecosystem (YARP)
  • Reduced client complexity

Cons:

  • Additional network latency
  • Single point of failure risk
  • Operational complexity
  • Potential bottleneck
  • Additional infrastructure component to manage

Complete Runnable Sample

Production-Ready YARP Gateway:

// ApiGateway/Program.cs - Complete implementation
using Yarp.ReverseProxy.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

// Add authentication and authorization
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
});

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
policy.RequireClaim("role", "admin"));
options.AddPolicy("RequireUser", policy =>
policy.RequireAuthenticatedUser());
options.AddPolicy("RequireCustomer", policy =>
policy.RequireClaim("role", "customer"));
});

// Add rate limiting with different policies
builder.Services.AddRateLimiter(options =>
{
// Global rate limiter
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: GetPartitionKey(context),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1)
}));

// Admin policy - higher limits
options.AddPolicy("AdminPolicy", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: GetPartitionKey(context),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 10000,
Window = TimeSpan.FromMinutes(1)
}));

// Customer policy - standard limits
options.AddPolicy("CustomerPolicy", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: GetPartitionKey(context),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));

options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Try again later.", token);
};
});

// Add CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("HotelManagementCors", policy =>
{
policy.WithOrigins(
builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>())
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});

// Add health checks
builder.Services.AddHealthChecks();

// Add custom services
builder.Services.AddSingleton<ITransformProvider, AddCustomHeaderTransform>();
builder.Services.AddScoped<ApiKeyValidationService>();

var app = builder.Build();

// Configure pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseCors("HotelManagementCors");

// Custom middleware
app.UseMiddleware<ApiGatewayMiddleware>();
app.UseMiddleware<ApiKeyValidationMiddleware>();

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

// Health checks
app.MapHealthChecks("/health");

// Map reverse proxy
app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline.Use(async (context, next) =>
{
// Custom proxy middleware
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString();
context.Request.Headers["X-Correlation-ID"] = correlationId;

await next();

context.Response.Headers["X-Correlation-ID"] = correlationId;
});
});

app.Run();

// Helper method for rate limiting partition key
static string GetPartitionKey(HttpContext context)
{
return context.User?.Identity?.Name ??
context.Request.Headers["X-API-Key"].FirstOrDefault() ??
context.Connection.RemoteIpAddress?.ToString() ??
"anonymous";
}

// Services/ApiKeyValidationService.cs
public class ApiKeyValidationService
{
private readonly IConfiguration _configuration;
private readonly ILogger<ApiKeyValidationService> _logger;
private readonly Dictionary<string, ApiKeyInfo> _validApiKeys;

public ApiKeyValidationService(IConfiguration configuration, ILogger<ApiKeyValidationService> logger)
{
_configuration = configuration;
_logger = logger;
_validApiKeys = LoadApiKeysFromConfiguration();
}

public bool IsValidApiKey(string apiKey, out ApiKeyInfo? keyInfo)
{
keyInfo = null;
if (string.IsNullOrEmpty(apiKey))
return false;

if (_validApiKeys.TryGetValue(apiKey, out keyInfo))
{
if (keyInfo.ExpiresAt > DateTime.UtcNow)
{
return true;
}
else
{
_logger.LogWarning("Expired API key used: {ApiKeyId}", keyInfo.Id);
}
}
else
{
_logger.LogWarning("Invalid API key used: {ApiKey}", apiKey[..8] + "...");
}

return false;
}

private Dictionary<string, ApiKeyInfo> LoadApiKeysFromConfiguration()
{
var apiKeys = new Dictionary<string, ApiKeyInfo>();
var apiKeySection = _configuration.GetSection("ApiKeys");

foreach (var section in apiKeySection.GetChildren())
{
var keyInfo = new ApiKeyInfo
{
Id = section["Id"] ?? string.Empty,
Name = section["Name"] ?? string.Empty,
Key = section["Key"] ?? string.Empty,
ExpiresAt = DateTime.Parse(section["ExpiresAt"] ?? DateTime.MaxValue.ToString()),
Scopes = section.GetSection("Scopes").Get<string[]>() ?? Array.Empty<string>()
};

if (!string.IsNullOrEmpty(keyInfo.Key))
{
apiKeys[keyInfo.Key] = keyInfo;
}
}

return apiKeys;
}
}

public class ApiKeyInfo
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
public string[] Scopes { get; set; } = Array.Empty<string>();
}

// Middleware/ApiKeyValidationMiddleware.cs
public class ApiKeyValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly ApiKeyValidationService _apiKeyService;
private readonly ILogger<ApiKeyValidationMiddleware> _logger;

public ApiKeyValidationMiddleware(
RequestDelegate next,
ApiKeyValidationService apiKeyService,
ILogger<ApiKeyValidationMiddleware> logger)
{
_next = next;
_apiKeyService = apiKeyService;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
// Skip API key validation for health checks and auth endpoints
if (context.Request.Path.StartsWithSegments("/health") ||
context.Request.Path.StartsWithSegments("/auth"))
{
await _next(context);
return;
}

var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();

if (!string.IsNullOrEmpty(apiKey))
{
if (_apiKeyService.IsValidApiKey(apiKey, out var keyInfo))
{
// Add API key info to context for downstream services
context.Items["ApiKeyInfo"] = keyInfo;
context.Request.Headers["X-API-Key-Id"] = keyInfo!.Id;
context.Request.Headers["X-API-Key-Name"] = keyInfo.Name;

_logger.LogInformation("Valid API key used: {ApiKeyName}", keyInfo.Name);
}
else
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Invalid API key");
return;
}
}
// If no API key provided, let it pass through for JWT validation

await _next(context);
}
}

// docker-compose.yml for complete setup
version: '3.8'
services:
api-gateway:
build:
context: ./ApiGateway
dockerfile: Dockerfile
ports:
- "5000:8080"
- "5001:8081"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:8081;http://+:8080
- Auth__Authority=https://your-identity-server
- Auth__Audience=hotel-management-api
depends_on:
- booking-service
- room-service
- customer-service
networks:
- hotel-network

booking-service:
build: ./BookingService
environment:
- ASPNETCORE_ENVIRONMENT=Development
networks:
- hotel-network

room-service:
build: ./RoomService
environment:
- ASPNETCORE_ENVIRONMENT=Development
networks:
- hotel-network

customer-service:
build: ./CustomerService
environment:
- ASPNETCORE_ENVIRONMENT=Development
networks:
- hotel-network

networks:
hotel-network:
driver: bridge