Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy:
- Different base URLs
- Duplicated auth logic
- No unified rate limiting / caching
- Hard to evolve routes or aggregate data
👉 Enter the API Gateway — your single front door for all microservices.
An API Gateway handles:
✅ Routing & path rewriting
✅ Load balancing, retries, circuit breakers
✅ Authentication & Authorization (JWT, OAuth2)
✅ Rate limiting & caching
✅ Aggregation (compose results from multiple services)
In this post we’ll implement two strong options:
- Ocelot → config-driven, mature, DevOps-friendly
- YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway
⚖️ Ocelot vs YARP — When to Choose
- Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code.
- YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashboards, policies).
Both are production-ready ✅
🛠 Example Setup
Assume services running locally:
product-service→ http://localhost:5001order-service→ http://localhost:5002
We’ll build a gateway at http://localhost:5000 exposing:
GET /api/products→ product-service/productsGET /api/orders→ order-service/ordersGET /api/market→ aggregate both (Ocelot demo)
🔹 Option A: Ocelot (Config-First Gateway)
1) Create Gateway Project
dotnet new web -n ApiGateway.Ocelot
cd ApiGateway.Ocelot
dotnet add package Ocelot
dotnet add package Ocelot.Provider.Consul # optional, service discovery
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
2) Core Routing Config
📄 ocelot.json
{
"Routes": [
{
"DownstreamPathTemplate": "/products",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [{ "Host": "localhost", "Port": 5001 }],
"UpstreamPathTemplate": "/api/products",
"UpstreamHttpMethod": [ "GET" ]
},
{
"DownstreamPathTemplate": "/orders",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [{ "Host": "localhost", "Port": 5002 }],
"UpstreamPathTemplate": "/api/orders",
"UpstreamHttpMethod": [ "GET" ]
}
],
"GlobalConfiguration": { "BaseUrl": "http://localhost:5000" }
}
3) Program.cs
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Authority = "https://your-identity-server";
o.Audience = "api";
o.RequireHttpsMetadata = false; // dev only
});
builder.Services.AddOcelot();
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
await app.UseOcelot(); // must be last
app.Run();
Run:
dotnet run
curl http://localhost:5000/api/products
curl http://localhost:5000/api/orders
4) Add QoS & Rate Limiting
Update route in ocelot.json:
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3,
"DurationOfBreak": 10000,
"TimeoutValue": 2000
},
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "1s",
"Limit": 10
}
➡ Timeout 2s, break after 3 failures for 10s
➡ Limit 10 requests/second
5) Aggregation Example
Combine products + orders:
{
"Routes": [
{
"DownstreamPathTemplate": "/products",
"UpstreamPathTemplate": "/internal/products",
"UpstreamHttpMethod": [ "GET" ],
"Key": "products"
},
{
"DownstreamPathTemplate": "/orders",
"UpstreamPathTemplate": "/internal/orders",
"UpstreamHttpMethod": [ "GET" ],
"Key": "orders"
},
{
"UpstreamPathTemplate": "/api/market",
"UpstreamHttpMethod": [ "GET" ],
"Aggregator": "MarketAggregator",
"RouteKeys": [ "products", "orders" ]
}
],
"GlobalConfiguration": { "BaseUrl": "http://localhost:5000" }
}
📄 MarketAggregator.cs
public class MarketAggregator : IDefinedAggregator
{
public async Task<Response> Aggregate(List<HttpContext> responses)
{
var result = new
{
products = await JsonSerializer.DeserializeAsync<object>(responses[0].Response.Body),
orders = await JsonSerializer.DeserializeAsync<object>(responses[1].Response.Body)
};
var json = JsonSerializer.Serialize(result);
var bytes = Encoding.UTF8.GetBytes(json);
var context = responses.First();
context.Response.Headers["Content-Type"] = "application/json";
await context.Response.Body.WriteAsync(bytes);
return new OkResponse();
}
}
Register in Program.cs:
builder.Services.AddOcelot().AddSingletonDefinedAggregator<MarketAggregator>();
Now:
curl http://localhost:5000/api/market
6) Service Discovery (Consul)
"ServiceDiscoveryProvider": {
"Type": "Consul",
"Host": "localhost",
"Port": 8500
}
Route example:
{
"ServiceName": "product-service",
"DownstreamPathTemplate": "/products",
"UseServiceDiscovery": true,
"UpstreamPathTemplate": "/api/products",
"UpstreamHttpMethod": [ "GET" ]
}
🔹 Option B: YARP (Code-First Gateway)
1) Create Project
dotnet new web -n ApiGateway.Yarp
cd ApiGateway.Yarp
dotnet add package Yarp.ReverseProxy
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
2) Config
📄 appsettings.json
"ReverseProxy": {
"Routes": {
"products": {
"ClusterId": "products-cluster",
"Match": { "Path": "/api/products/{**catch-all}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
},
"orders": {
"ClusterId": "orders-cluster",
"Match": { "Path": "/api/orders/{**catch-all}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
}
},
"Clusters": {
"products-cluster": {
"Destinations": {
"d1": { "Address": "http://localhost:5001/" }
}
},
"orders-cluster": {
"Destinations": {
"d1": { "Address": "http://localhost:5002/" }
}
}
}
}
3) Program.cs
using Yarp.ReverseProxy;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Authority = "https://your-identity-server";
o.Audience = "api";
});
builder.Services.AddHttpClient("yarp")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(new[] { TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(1) }));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Propagate correlation ID
app.Use(async (ctx, next) =>
{
if (!ctx.Request.Headers.ContainsKey("X-Correlation-ID"))
ctx.Request.Headers["X-Correlation-ID"] = Guid.NewGuid().ToString("n");
await next();
});
app.MapReverseProxy();
app.Run();
🔐 Auth Patterns at the Gateway
- Validate tokens once at gateway
- Forward claims downstream via:
- Original
Authorizationheader - Short-lived downstream token
- Custom headers (inside trusted networks)
- Original
🚀 Extra Features
- Caching & compression (Ocelot built-in, YARP via middleware)
- Observability: add OpenTelemetry at the gateway for full latency breakdown
- Timeouts & retries: fail fast, avoid cascading slowness
- Security: add HSTS, CORS, CSP headers
🐳 Docker Compose (Quick Local Stack)
📄 docker-compose.yml
version: "3.9"
services:
product-service:
image: yourrepo/product-service:latest
ports: [ "5001:80" ]
order-service:
image: yourrepo/order-service:latest
ports: [ "5002:80" ]
api-gateway:
build: ./ApiGateway.Ocelot
environment:
- ASPNETCORE_URLS=http://+:80
ports:
- "5000:80"
depends_on:
- product-service
- order-service
Test:
curl http://localhost:5000/api/products
curl http://localhost:5000/api/orders
✅ Production Tips
- Keep gateway thin → only routing, auth, policies
- Use timeouts & circuit breakers → don’t let one bad service block all
- Rate limit per API key/tenant → protect downstreams
- Versioned routes →
/api/v1/*,/api/v2/* - Zero-trust → validate at edge, re-issue constrained tokens
- Observability → traces across Gateway → Service → DB
🎯 Recap
- Ocelot → config-driven, quick setup, built-in QoS
- YARP → code-first, ultimate extensibility
- Both → handle auth, routing, load balancing, caching
👉 The API Gateway is your secure, observable front door.
Next up Link → Part 10: Event-Driven Microservices with RabbitMQ/Kafka ⚡
Comments
Post a Comment