API Gateway with YARP
Instead of the user talking to ten separate services, all requests pass through one entry door. This entry door finds the route and does shared jobs like authentication and rate limiting in one place. In .NET we build it with the YARP library.
Author: bezzad
The problem: a mobile app with three services
Our shop has three services: catalog, orders and payment. The mobile app must talk to all three. If the app connects to the services directly:
- The app must know the address of every service. If tomorrow we split the order service in two, we must publish a new app version and wait for users to install it.
- Shared jobs are repeated. Each service must check the token on its own, set rate limits, and have CORS settings. Each team does this a little differently.
- All services are visible from the internet. Each service is an entry door for attacks.
The idea: one entry door
An API Gateway sits in front of all services. The user knows only one address. The services are only on the internal network.
Jobs that the Gateway does:
- Routing. Addresses that start with “/api/orders” go to the order service.
- Authentication. It checks the token once. A request without a valid token does not reach the services.
- Rate limiting. It stops a user who sends a thousand requests in one minute.
- Load balancing. If the order service has three instances, it spreads requests between them and leaves out an unhealthy instance.
- Request changes. For example, it removes the “/api” prefix from the address or adds a header.
The path of one request
Let us follow one request. A customer opens order number 42:
- If the token is not valid, the Gateway returns code 401 right at the start.
- If the user has gone over their quota, they get code 429.
- Then the Gateway finds a route that matches the address.
- It changes the address for the target service.
- It picks one of the healthy instances of the order service and sends the request.
So the order service only sees correct requests, and it focuses on its own main job.
The YARP library
The name YARP is short for Yet Another Reverse Proxy. It is an open-source library from Microsoft, built on ASP.NET Core. So our Gateway is a normal ASP.NET Core app, and everything we know (Middleware, authentication, Rate Limiting, logging) works in it.
YARP has three main concepts:
- Route. An address pattern, like “/api/orders/” and anything after it. Each Route points to a Cluster.
- Cluster. The group of instances of one service, with the load balancing method and the health check.
- Destination. The real address of each instance.
Code
The Gateway app
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// JWT settings come from configuration (Authentication:Schemes:Bearer).
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
o.AddPolicy("customer", p => p.RequireAuthenticatedUser()));
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddPolicy("per-user", context => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? "anonymous",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.UseAuthentication(); // first: who is the user?
app.UseAuthorization();
app.UseRateLimiter(); // must run before the proxy
app.MapReverseProxy();
app.Run();
Route settings
{
"ReverseProxy": {
"Routes": {
"catalog": {
"ClusterId": "catalog",
"Match": { "Path": "/api/catalog/{**rest}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
},
"orders": {
"ClusterId": "orders",
"AuthorizationPolicy": "customer",
"RateLimiterPolicy": "per-user",
"Match": { "Path": "/api/orders/{**rest}" },
"Transforms": [ { "PathRemovePrefix": "/api" } ]
}
},
"Clusters": {
"catalog": {
"Destinations": {
"c1": { "Address": "http://catalog-1:8080/" },
"c2": { "Address": "http://catalog-2:8080/" }
}
},
"orders": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": {
"Active": {
"Enabled": true,
"Interval": "00:00:10",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health/ready"
}
},
"Destinations": {
"o1": { "Address": "http://orders-1:8080/" },
"o2": { "Address": "http://orders-2:8080/" }
}
}
}
}
}
These settings say:
- The catalog is open to everyone. Orders are only for signed-in users, and each user has a quota of one hundred requests per minute.
- The address “/api/orders/42” reaches the order service as “/orders/42”.
- The health of the order instances is checked every ten seconds. An instance that fails to answer several times in a row is left out.
Important rules
- Do not put business logic in the Gateway. Pricing or order rules belong in the service. If the Gateway knows business rules, every small change must be made in two places.
- Services must check permissions too. The Gateway only says “this user is signed in”. The order service must check “is this user allowed to see order 42?”. If one day someone reaches the service directly from inside the network, it must not be defenseless.
- Run several instances of the Gateway. All traffic passes through it. If there is only one instance and it goes down, the whole shop goes down.
- Keep it light. Each request has one extra network step. Heavy work in the Gateway makes all services slow.
- Think about a separate Gateway for each kind of user. If the mobile app and the website have very different needs, the BFF (Backend for Frontend) pattern means one Gateway for each. This way the mobile team does not wait for the web team.
- Create the request ID right here. A trace ID that starts at the Gateway makes it possible to follow one request across all services.
Common mistakes
| Mistake | Result | Right way |
|---|---|---|
| Business logic in the Gateway | A “god service” that every team must change. | The Gateway only does shared jobs and routing. |
| Only the Gateway checks permissions | Direct access from inside the network with no check at all. | Services check permissions too. |
| One instance of the Gateway | When it goes down, everything goes down. | Several instances behind a Load Balancer. |
| One shared Gateway for all clients, with a lot of code | Teams wait for each other and the Gateway gets big. | The BFF pattern for each kind of client. |
| Rate Limiter before authentication | Per-user quota does not work. | First authentication, then the limit. |
When to use a Gateway?
Good fit
- Several services that outside clients (mobile, web, partners) need.
- Shared jobs like authentication and rate limiting that you do not want to repeat in each service.
- A need to move or split services without changing the client.
Bad fit
- A Monolith with one app. An extra layer only adds complexity and delay.
- When your infrastructure already has an Ingress or a managed Gateway that does the same jobs.
Summary in six lines
- An API Gateway is the entry door to all services. The client knows only one address.
- Shared jobs in one place: routing, authentication, rate limiting, load balancing.
- In YARP, each Route leads to a Cluster, and each Cluster has several destinations.
- Because YARP is built on ASP.NET Core, all the familiar Middleware works in it.
- Business logic is not in the Gateway, and services also check permissions themselves.
- Run several instances of the Gateway, and for very different clients, think about the BFF pattern.