Building a custom API Gateway with YARP

Whenever you are designing an architecture with microservices, you might encounter in how to implement an API Gateway, since you need a way to communicate and consume multiple services, generally through APIs. A possible solution is to have a single entry point for all your clients and implement an API Gateway, which will handle all the requests and route those to appropiate microservices.

There are different ways to implement an API Gateway or pay for built-in services in cloud hostings.

In this post I will pick the easiest way that I found to create one for a microservice architecture using .NET and YARP. Here is a general overview of a microservice architecture.

architecture

YARP

YARP (which stands for “Yet Another Reverse Proxy”) is an open-source project that Microsoft built for improving routing through internal services using a built-in reverse proxy server. This become very popular and was implemented for several Azure products as App Service.

dotnet new web -n ApiGateway -f net7.0
Install-Package Yarp.ReverseProxy
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Yarp.ReverseProxy" Version="2.0.1" />
  </ItemGroup>

</Project>
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
{
  "ReverseProxy": {
    "Routes": {
      "product-route": {
        "ClusterId": "product-cluster",
        "Match": {
          "Path": "/p/{**catch-all}",
          "Hosts": ["*"]
        },
        "Transforms": [
          {
            "PathPattern": "{**catch-all}"
          }
        ]
      },
      "employee-route": {
        "ClusterId": "employee-cluster",
        "Match": {
          "Path": "/e/{**catch-all}",
          "Hosts": ["*"]
        },
        "Transforms": [
          {
            "PathPattern": "{**catch-all}"
          }
        ]
      }
    },
    "Clusters": {
      "product-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "http://localhost:3500/v1.0/invoke/product-api/method/"
          }
        }
      },
      "employee-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "http://localhost:3500/v1.0/invoke/employee-api/method/"
          }
        }
      }
    }
  }
}
var builder = WebApplication.CreateBuilder(args);
var allowOrigins = "_allowOrigins";
builder.Services.AddCors(options =>
{
    options.AddPolicy(allowOrigins, policy =>
    {
        policy
          .WithOrigins("http://localhost:3000", "http://127.0.0.1")
          .SetIsOriginAllowedToAllowWildcardSubdomains()
          .AllowAnyHeader()
          .WithMethods("GET", "POST", "PUT", "DELETE")
          .AllowCredentials();
    });
});
var app = builder.Build();
app.UseCors(allowOrigins);
"Routes": {
      "product-route": {
        "ClusterId": "product-cluster",
        "CorsPolicy": "_allowOrigins",
        "Match": { "..." },
        "Transforms": [ "..."]
      },
}
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Yarp": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }

Your donations are appreciated. Thank you!