Levelwise
English
Infrastructure and DevOps

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.

Not reviewedWritten with AI helpReading time: 13 minOnline shop mobile app exampleC# code with YARP and .NET 10

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:

  1. 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.
  2. 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.
  3. 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.

Without a shared entry doorWith one entry doorMobile appCatalogOrdersPaymentThree addresses, three auth checksAll services visible from the internetMobile appAPI GatewayCatalogOrdersPaymentOne address, shared jobs in one placeServices only on the internal network
With a Gateway, the mobile app knows only one address, and the services are hidden behind it.

Jobs that the Gateway does:

  1. Routing. Addresses that start with “/api/orders” go to the order service.
  2. Authentication. It checks the token once. A request without a valid token does not reach the services.
  3. Rate limiting. It stops a user who sends a thousand requests in one minute.
  4. Load balancing. If the order service has three instances, it spreads requests between them and leaves out an unhealthy instance.
  5. 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:

GET /api/orders/42AuthenticateJWTLimitRate LimitFind routeRouteRewrite pathTransformPick targetLoad BalanceOrder service/orders/42401429Bad requests stop here, never reach the service
A bad request is rejected early. Only a correct request reaches the service.
  1. If the token is not valid, the Gateway returns code 401 right at the start.
  2. If the user has gone over their quota, they get code 429.
  3. Then the Gateway finds a route that matches the address.
  4. It changes the address for the target service.
  5. 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.
RoutesClustersDestinations/api/catalog/{**rest}/api/orders/{**rest}catalogorderscatalog-1:8080catalog-2:8080orders-1:8080orders-2:8080No health reply: left outWhich path goes to which serviceInstances of one serviceAll of this is defined only in settings, with no extra code
Each Route leads to a Cluster. Each Cluster has several destinations, and an unhealthy destination is left out.

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.
Middleware order matters: First authentication, then rate limiting. If not, the Rate Limiter does not know who the user is, and it counts everyone as one.

Important rules

  1. 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.
  2. 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.
  3. 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.
  4. Keep it light. Each request has one extra network step. Heavy work in the Gateway makes all services slow.
  5. 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.
  6. Create the request ID right here. A trace ID that starts at the Gateway makes it possible to follow one request across all services.
Combining responses: YARP is a reverse proxy. It sends each request to one destination. If one page needs data from three services and you want to give one combined reply, you must write a normal endpoint yourself, in the Gateway or in a BFF.

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

  1. An API Gateway is the entry door to all services. The client knows only one address.
  2. Shared jobs in one place: routing, authentication, rate limiting, load balancing.
  3. In YARP, each Route leads to a Cluster, and each Cluster has several destinations.
  4. Because YARP is built on ASP.NET Core, all the familiar Middleware works in it.
  5. Business logic is not in the Gateway, and services also check permissions themselves.
  6. Run several instances of the Gateway, and for very different clients, think about the BFF pattern.