API Gateway & Backend for Frontend
API Gateway serves as a single entry point for all clients, handling cross-cutting concerns. BFF (Backend for Frontend) creates specialized backends for different client types.
API Gateway Pattern
YARP-Based API Gateway
// Program.cs - YARP Configuration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(transforms =>
{
// Add correlation ID
transforms.AddRequestTransform(async context =>
{
var correlationId = context.HttpContext.Request
.Headers["X-Correlation-ID"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
context.ProxyRequest.Headers.Add("X-Correlation-ID", correlationId);
});
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Auth:Authority"];
options.Audience = builder.Configuration["Auth:Audience"];
});
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 100;
opt.QueueLimit = 10;
});
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
app.MapReverseProxy();
app.Run();
// appsettings.json - Route Configuration
{
"ReverseProxy": {
"Routes": {
"orders": {
"ClusterId": "orders-cluster",
"AuthorizationPolicy": "authenticated",
"RateLimiterPolicy": "api",
"Match": { "Path": "/api/orders/{**catch-all}" },
"Transforms": [{ "PathRemovePrefix": "/api" }]
},
"products": {
"ClusterId": "products-cluster",
"Match": { "Path": "/api/products/{**catch-all}" }
}
},
"Clusters": {
"orders-cluster": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": {
"Active": { "Enabled": true, "Interval": "00:00:10", "Path": "/health" }
},
"Destinations": {
"orders-1": { "Address": "http://orders-service:8080" },
"orders-2": { "Address": "http://orders-service-2:8080" }
}
},
"products-cluster": {
"Destinations": {
"products-1": { "Address": "http://products-service:8080" }
}
}
}
}
}
Request Aggregation
// Aggregate multiple service calls into single response
[ApiController]
[Route("api/dashboard")]
public class DashboardAggregatorController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly IProductService _productService;
private readonly IUserService _userService;
[HttpGet]
public async Task<IActionResult> GetDashboard()
{
var userId = User.GetUserId();
// Parallel calls to multiple services
var userTask = _userService.GetUserAsync(userId);
var ordersTask = _orderService.GetRecentOrdersAsync(userId, 5);
var recommendationsTask = _productService.GetRecommendationsAsync(userId, 10);
await Task.WhenAll(userTask, ordersTask, recommendationsTask);
return Ok(new DashboardResponse
{
User = new UserSummary
{
Name = userTask.Result.Name,
Email = userTask.Result.Email,
MemberSince = userTask.Result.CreatedAt
},
RecentOrders = ordersTask.Result.Select(o => new OrderSummary
{
OrderId = o.Id,
Total = o.TotalAmount,
Status = o.Status,
Date = o.CreatedAt
}).ToList(),
Recommendations = recommendationsTask.Result.Select(p => new ProductSummary
{
ProductId = p.Id,
Name = p.Name,
Price = p.Price,
ImageUrl = p.ThumbnailUrl
}).ToList()
});
}
}
Backend for Frontend (BFF)
// Mobile BFF - Optimized for mobile clients
[ApiController]
[Route("api/mobile")]
public class MobileBffController : ControllerBase
{
[HttpGet("home")]
public async Task<IActionResult> GetHomeScreen()
{
// Mobile-optimized response with minimal data
var userId = User.GetUserId();
var data = await Task.WhenAll(
_userService.GetUserSummaryAsync(userId),
_orderService.GetActiveOrderCountAsync(userId),
_productService.GetFeaturedProductsAsync(6) // Fewer items for mobile
);
return Ok(new MobileHomeResponse
{
UserName = data[0].Name,
ActiveOrders = (int)data[1],
FeaturedProducts = ((List<Product>)data[2]).Select(p => new
{
p.Id,
p.Name,
p.Price,
Image = p.ThumbnailUrl // Smaller images for mobile
})
});
}
}
// Web BFF - Richer response for desktop
[ApiController]
[Route("api/web")]
public class WebBffController : ControllerBase
{
[HttpGet("home")]
public async Task<IActionResult> GetHomePage()
{
var userId = User.GetUserId();
var data = await Task.WhenAll(
_userService.GetUserProfileAsync(userId),
_orderService.GetRecentOrdersAsync(userId, 10),
_productService.GetFeaturedProductsAsync(20),
_productService.GetCategoriesAsync(),
_analyticsService.GetPersonalizedBannersAsync(userId)
);
return Ok(new WebHomeResponse
{
User = MapToUserProfile(data[0]),
RecentOrders = MapToOrderList(data[1]),
FeaturedProducts = MapToProductGrid(data[2]),
Categories = data[3],
Banners = data[4],
// Additional web-specific data
QuickLinks = GetQuickLinks(),
Notifications = await GetNotificationsAsync(userId)
});
}
}
GraphQL as BFF
// GraphQL Schema for flexible queries
public class Query
{
public async Task<User> GetUser(
[Service] IUserService userService,
Guid id)
{
return await userService.GetUserAsync(id);
}
[UsePaging]
[UseFiltering]
[UseSorting]
public IQueryable<Order> GetOrders(
[Service] IOrderRepository repository,
[GlobalState] Guid userId)
{
return repository.GetOrdersForUser(userId);
}
}
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(u => u.Id).Type<NonNullType<IdType>>();
descriptor.Field(u => u.Email).Type<NonNullType<StringType>>();
// Resolve orders from separate service
descriptor.Field("orders")
.ResolveWith<UserResolvers>(r => r.GetOrders(default!, default!))
.UsePaging();
}
}
public class UserResolvers
{
public async Task<IEnumerable<Order>> GetOrders(
[Parent] User user,
[Service] IOrderService orderService)
{
return await orderService.GetOrdersByUserAsync(user.Id);
}
}
Rate Limiting Strategies
// Advanced rate limiting
builder.Services.AddRateLimiter(options =>
{
// Per-user rate limit
options.AddPolicy("per-user", context =>
{
var userId = context.User.FindFirst("sub")?.Value ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
userId,
_ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 100
});
});
// Per-endpoint rate limit
options.AddPolicy("expensive-operation", _ =>
RateLimitPartition.GetTokenBucketLimiter(
"expensive",
_ => new TokenBucketRateLimiterOptions
{
TokenLimit = 10,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
TokensPerPeriod = 2
}));
// API key based limits
options.AddPolicy("api-key", context =>
{
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault();
var tier = GetTierForApiKey(apiKey);
return tier switch
{
"premium" => RateLimitPartition.GetNoLimiter("premium"),
"standard" => RateLimitPartition.GetFixedWindowLimiter(
apiKey,
_ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 1000
}),
_ => RateLimitPartition.GetFixedWindowLimiter(
apiKey ?? "anonymous",
_ => new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 60
})
};
});
});
Caching at Gateway
// Response caching middleware
public class GatewayCachingMiddleware
{
private readonly IDistributedCache _cache;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!IsCacheable(context.Request))
{
await next(context);
return;
}
var cacheKey = GenerateCacheKey(context.Request);
var cached = await _cache.GetAsync(cacheKey);
if (cached != null)
{
context.Response.ContentType = "application/json";
await context.Response.Body.WriteAsync(cached);
return;
}
var originalBody = context.Response.Body;
using var memStream = new MemoryStream();
context.Response.Body = memStream;
await next(context);
if (context.Response.StatusCode == 200)
{
memStream.Position = 0;
var responseBody = memStream.ToArray();
await _cache.SetAsync(
cacheKey,
responseBody,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
}
context.Response.Body = originalBody;
}
}
Key Takeaways
- Single Entry Point: Gateway centralizes cross-cutting concerns
- Client-Specific BFFs: Optimize APIs for each client type
- Request Aggregation: Reduce client round trips
- Rate Limiting: Protect services from overload
- Caching: Reduce load on backend services
💡 Flashcard
What is the main purpose of an API Gateway?
Click to reveal answer✅ Answer
API Gateway serves as a single entry point for all clients, handling cross-cutting concerns like authentication, rate limiting, caching, load balancing, and request routing to appropriate backend services.
Click to see question💡 Flashcard
When should you use Backend for Frontend (BFF) pattern?
Click to reveal answer✅ Answer
Use BFF when different clients (web, mobile, IoT) have significantly different data needs, response format requirements, or optimization concerns. Each BFF is tailored to its specific client's needs.
Click to see questionLoading quiz...