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.
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.
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.
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.
| Job | What it means | Why do it at the gateway |
|---|---|---|
| Routing | Send each request to the right service | Clients need only one address |
| Authentication | Check who the caller is | Block bad requests before they reach services |
| Rate limiting | Stop one caller flooding you | Protect every service at once |
| Load balancing | Spread traffic across copies | Keep services fast and healthy |
| Logging | Record what comes in and goes out | One 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
Steps
Request
Client calls /products/42
Route
Path /products matches
Cluster
Route points to products cluster
Destination
Forward to service address
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.ReverseProxyIn .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.
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
Steps
Request
No valid token sent
Gateway Auth
Token is missing or wrong
Rejected
Return 401 Unauthorized
Service
Never reached, stays safe
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.
| Policy | How it shares traffic | Good for |
|---|---|---|
PowerOfTwoChoices | Picks two at random, sends to the less busy one | The safe default for most apps |
RoundRobin | Goes in order, one after another | Simple, even, easy to predict |
LeastRequests | Sends to the copy with the fewest in-flight requests | When some requests are slow |
Random | Picks any copy at random | Very 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.
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,/productsmatches but/products/42does not. The catch-all makes the route cover everything under the prefix. - Putting
app.MapReverseProxy()beforeUseAuthenticationandUseAuthorization. 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
appsettingsvalues. - 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(...)andMapReverseProxy(). - 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
/apiprefix 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
- Get started with YARP — Microsoft Learn
- YARP Configuration Files — Microsoft Learn
- YARP project home
- YARP as API Gateway in .NET — antondevtips
- Implementing an API Gateway for Microservices with YARP — Milan Jovanović
Related Posts
Horizontally Scaling ASP.NET Core APIs With YARP Load Balancing
Learn how to scale ASP.NET Core APIs horizontally using YARP load balancing, with policies, health checks, and a full Program.cs setup explained simply.
Getting Started With Dapr for Building Cloud-Native Microservices in .NET
A beginner-friendly guide to Dapr for .NET developers: learn sidecars, state, pub/sub, and service invocation to build cloud-native microservices.
Rate Limiting in ASP.NET Core: A Simple, Complete Guide
Learn rate limiting in ASP.NET Core with simple examples. Understand fixed window, sliding window, token bucket, and concurrency limiters, with diagrams, code, and real-world advice on which to pick.
YARP as an API Gateway in .NET: A Beginner's Guide
Learn how to use YARP as an API gateway in .NET 10. Routes, clusters, load balancing, health checks, auth, and transforms explained in simple, friendly steps.
Implementing API Gateway Authentication With YARP in .NET
Learn to build a secure API gateway in .NET using YARP. Add authentication, per-route authorization policies, and pass user identity to backend services.
YARP vs Nginx: A Quick Performance Comparison for .NET
A simple, friendly look at YARP vs Nginx as a reverse proxy: how each one works, real benchmark numbers, tuning tips, and how to pick the right one.