Skip to main content
SEMastery
DevOpsintermediate

Implementing an API Gateway for Microservices With YARP

Learn to build an API gateway for microservices with YARP in .NET 10. Routes, clusters, auth, rate limits, and transforms explained in simple steps.

12 min readUpdated September 30, 2025

One reception desk for the whole hospital

Think about a big hospital in your city. When you walk in, you do not wander the corridors looking for the X-ray room, the blood test lab, or the children's ward. You go to one reception desk at the front.

You tell the person at the desk what you need. They look at your problem, give you a token, and point you to the right department. If you have no appointment, they stop you. If too many people arrive at once, they manage the queue. You never have to know where each department actually sits inside the building.

That reception desk is doing a very useful job. It is the single front door. Every visitor goes through it, and it sends each person to the right place.

In the world of microservices, this front door is called an API gateway. Your app might have a products service, an orders service, and a users service, each running separately. You do not want phones and browsers talking to all of them directly. Instead, they talk to one gateway, and the gateway forwards each request to the correct service.

In .NET, one clean way to build this gateway is YARP, which stands for Yet Another Reverse Proxy. It is a free library from Microsoft, and it runs inside a normal ASP.NET Core app.

What problem does a gateway solve?

Without a gateway, every client must know the address of every service. That sounds fine with two services. It becomes a mess with twenty.

Without a gateway, clients must know and call every service directly.

Look at all those lines. Each client connects to each service. If a service moves to a new address, every client breaks. Login checks have to be copied into every service. It is hard to change anything safely.

Now add a gateway in the middle.

With a gateway, every client talks to one front door, which forwards to the services.

Much cleaner. Clients only know one address: the gateway. The gateway knows where everything lives. This gives you one good place to handle shared jobs.

Here are the common jobs a gateway takes care of.

JobWhat it meansWhy do it at the gateway
RoutingSend each request to the right serviceClients need only one address
AuthenticationCheck who the caller isBlock bad requests before they reach services
Rate limitingStop one caller flooding youProtect every service at once
Load balancingSpread traffic across copiesKeep services fast and healthy
LoggingRecord what comes in and goes outOne clear view of all traffic

Two key words: routes and clusters

YARP has just two main ideas, and once you get them, everything else falls into place.

A route describes which requests to catch. You usually match on the URL path. For example, "any request whose path starts with /products."

A cluster describes where to send those requests. It lists one or more backend addresses, called destinations. For example, the products service running at https://localhost:5101.

A route points at a cluster using a ClusterId. So the route says what to catch, and the cluster says where to forward it.

How a request flows through YARP

Request
Route
Cluster
Destination

Steps

1

Request

Client calls /products/42

2

Route

Path /products matches

3

Cluster

Route points to products cluster

4

Destination

Forward to service address

A request is matched by a route, sent to a cluster, then forwarded to a destination.

Setting up the gateway project

Start by creating an empty ASP.NET Core web app for the gateway. This app does almost no work of its own. Its whole job is to forward requests.

dotnet new web -n ApiGateway
cd ApiGateway
dotnet add package Yarp.ReverseProxy

In .NET 10, YARP ships as a supported package, and the basic setup is short. Open Program.cs and wire YARP in.

var builder = WebApplication.CreateBuilder(args);
 
// Read the routes and clusters from appsettings.json
builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
var app = builder.Build();
 
// This line turns the app into a working reverse proxy
app.MapReverseProxy();
 
app.Run();

That is the entire engine. The two lines that matter are AddReverseProxy().LoadFromConfig(...), which loads your rules, and MapReverseProxy(), which puts the proxy into the request pipeline. Everything else now lives in configuration.

Writing your first routes and clusters

Open appsettings.json and add a ReverseProxy section. Here we set up two services: products and orders.

{
  "ReverseProxy": {
    "Routes": {
      "products-route": {
        "ClusterId": "products-cluster",
        "Match": {
          "Path": "/products/{**catch-all}"
        }
      },
      "orders-route": {
        "ClusterId": "orders-cluster",
        "Match": {
          "Path": "/orders/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "products-cluster": {
        "Destinations": {
          "d1": { "Address": "https://localhost:5101" }
        }
      },
      "orders-cluster": {
        "Destinations": {
          "d1": { "Address": "https://localhost:5201" }
        }
      }
    }
  }
}

Read it slowly. Under Routes, each route has a name, a ClusterId it points to, and a Match block. The Path uses {**catch-all}, which means "match this prefix and anything after it." So products-route catches /products, /products/42, and /products/42/reviews.

Under Clusters, each cluster lists its destinations with an Address. When a request matches products-route, YARP forwards it to the address in products-cluster.

So a call to GET /products/42 on the gateway becomes a call to https://localhost:5101/products/42 on the products service. The path is kept by default, which is exactly what you usually want.

Two routes point to two clusters, each with its own backend service.

Changing the request on the way through

Sometimes the path on the gateway should not match the path on the service. Maybe clients call /api/products, but the products service only knows /products. YARP fixes this with transforms. A transform changes the request before it is forwarded.

A common transform is PathRemovePrefix. It strips a part of the path before sending the request on.

"products-route": {
  "ClusterId": "products-cluster",
  "Match": {
    "Path": "/api/products/{**catch-all}"
  },
  "Transforms": [
    { "PathRemovePrefix": "/api" }
  ]
}

Now a call to GET /api/products/42 reaches the gateway, the /api part is removed, and the products service receives GET /products/42. Transforms can also add headers, remove headers, or pass the original host along. They keep your public URLs tidy while letting each service keep its own simple paths.

Adding authentication at the gateway

One big reason teams love a gateway is shared security. Instead of putting login checks inside every service, you check once at the gateway. If the caller is not allowed in, the request never reaches any service.

First, add normal ASP.NET Core authentication and authorization in Program.cs. Here we use JWT bearer tokens, which is the usual choice for APIs.

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://your-identity-server";
        options.Audience = "gateway";
    });
 
builder.Services.AddAuthorization();
 
builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
var app = builder.Build();
 
app.UseAuthentication();
app.UseAuthorization();
 
app.MapReverseProxy();
 
app.Run();

Then attach a policy to the routes that need it. In YARP you do this with AuthorizationPolicy on the route.

"orders-route": {
  "ClusterId": "orders-cluster",
  "AuthorizationPolicy": "default",
  "Match": {
    "Path": "/orders/{**catch-all}"
  }
}

The value "default" means "the caller must be logged in." You can also name your own policies, for example one that requires an admin role, and use that name here. Public routes like a health page can be left open by setting the policy to "anonymous".

A request that fails the auth check

Request
Gateway Auth
Rejected
Service

Steps

1

Request

No valid token sent

2

Gateway Auth

Token is missing or wrong

3

Rejected

Return 401 Unauthorized

4

Service

Never reached, stays safe

The gateway stops an unauthenticated request before it touches a service.

Protecting services with rate limiting

A gateway is also the perfect place to stop one noisy caller from flooding your system. ASP.NET Core has built-in rate limiting, and YARP can attach a limiter to a route.

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", limiter =>
    {
        limiter.PermitLimit = 100;   // allow 100 requests
        limiter.Window = TimeSpan.FromMinutes(1); // per minute
    });
});
 
var app = builder.Build();
 
app.UseRateLimiter();
app.MapReverseProxy();

Then point a route at the limiter with RateLimiterPolicy.

"products-route": {
  "ClusterId": "products-cluster",
  "RateLimiterPolicy": "fixed",
  "Match": {
    "Path": "/products/{**catch-all}"
  }
}

Now any caller can make at most 100 requests per minute to /products. Extra requests get a 429 Too Many Requests reply, and your products service stays calm. Because this lives at the gateway, every service behind it is protected with one small piece of configuration.

Load balancing across many copies

When one copy of a service is not enough, you run several copies and let the gateway share traffic between them. In YARP you simply add more destinations to the cluster.

"products-cluster": {
  "LoadBalancingPolicy": "RoundRobin",
  "Destinations": {
    "d1": { "Address": "https://localhost:5101" },
    "d2": { "Address": "https://localhost:5102" },
    "d3": { "Address": "https://localhost:5103" }
  }
}

Now the gateway spreads requests across three copies of the products service. The LoadBalancingPolicy decides how. Here are the common choices.

PolicyHow it shares trafficGood for
PowerOfTwoChoicesPicks two at random, sends to the less busy oneThe safe default for most apps
RoundRobinGoes in order, one after anotherSimple, even, easy to predict
LeastRequestsSends to the copy with the fewest in-flight requestsWhen some requests are slow
RandomPicks any copy at randomVery simple needs

If you do not set a policy, YARP uses PowerOfTwoChoices, which is a smart default. To keep traffic away from broken copies, you would also add health checks, which let YARP stop using a copy that is failing until it recovers.

How it all fits together

Here is the full journey of one request through a gateway that does auth, rate limiting, routing, and load balancing.

A single request passing through the gateway stages before reaching a service.

Notice how much the gateway does before the service is even touched. The service only ever sees clean, allowed, well-shaped traffic. That is the whole point: keep the shared, fiddly work in one place so each service can stay small and focused.

A note on tools and licensing

A quick, honest heads-up since you are building microservices. Some popular .NET libraries that often appear in this space, such as MediatR and MassTransit, have moved to a commercial license for many uses. They are still good tools, but check their license terms before you depend on them in a paid product.

YARP itself is different here. It is free and open source under the .NET Foundation, and in .NET 10 it is well supported as a first-class way to build a gateway or reverse proxy. So you can lean on it without licensing worry.

Common mistakes to avoid

A few small things trip people up when they start.

  • Forgetting {**catch-all} in the path. Without it, /products matches but /products/42 does not. The catch-all makes the route cover everything under the prefix.
  • Putting app.MapReverseProxy() before UseAuthentication and UseAuthorization. Order matters. The auth middleware must run first so the proxy sees an already-checked request.
  • Hard-coding service addresses that change between dev and production. Keep them in configuration so each environment can have its own appsettings values.
  • Skipping health checks in production. Without them the gateway happily forwards traffic to a dead copy.

Quick recap

  • An API gateway is a single front door in front of all your microservices, just like one reception desk for a whole hospital.
  • YARP is a free Microsoft library that turns a normal ASP.NET Core app into a gateway, set up with AddReverseProxy().LoadFromConfig(...) and MapReverseProxy().
  • A route says which requests to catch, usually by path. A cluster says where to forward them, listing backend destinations.
  • Transforms reshape the request, for example removing a /api prefix before forwarding.
  • The gateway is the right place for shared jobs: authentication, rate limiting, load balancing, and logging, each added once and attached to routes in configuration.
  • Most routing and cluster setup lives in appsettings.json, so you can change it without rebuilding the app.
  • YARP is free and open source, while some other microservice tools like MediatR and MassTransit now need a commercial license, so check before you depend on them.

References and further reading

Related Posts