Swimburger

Use YARP to host client and API server on a single origin to avoid CORS

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

.NET Bot next to title: Use YARP to host client and API server on a single origin

Applications are often split up into a client and a server. The client renders the UI and communicates to the server through an API. But when the client and server are split up into separate projects and hosted at different origins (origin == scheme + host + port), it can be a hassle for the client to communicate with the server.

On the web specifically, communicating between different origins can be painful. By default, clients are not allowed to make ajax requests to a different origin. The server has to use Cross-Origin Resource Sharing (CORS) headers to allow cross-origin ajax requests.

Another way to resolve CORS challenges is to circumvent it entirely by having the client and server hosted on the same origin. If your technology stack allows for it, you can merge the client and server into a single project and host them on a single origin. If that's not possible, you can use a reverse proxy to consolidate the client origin and the server origin under the proxy's origin.

A diagram: web browser points to reverse proxy server. Reverse proxy server points to two web servers. The first web server is an API server. The second server is a frontend website server. The diagram visualizes how the HTTP requests from a web browser are sent to the reverse proxy. If the requested path starts with "/API/", the proxy forwards the request to the API server. Otherwise the request is sent to the Client Web Server which serves the frontend of your application.

The diagram above visualizes how the HTTP requests from a web browser are sent to the reverse proxy. If the requested path starts with '/API/', the proxy forwards the request to the API server. Otherwise, the request is sent to the Client Web Server which serves the frontend of your application.

You can pick any option for your frontend client, backend server, and reverse proxy server. It does not matter because they all understand the same language: HTTP. In this walkthrough, you'll learn how to communicate between a Blazor WebAssembly (WASM) client and an ASP.NET Web API without CORS. The client and server will be hosted on a single origin by putting Microsoft's new reverse proxy: YARP (which stands for "YARP: A Reverse Proxy")

A diagram: web browser points to the YARP reverse proxy. YARP server points to two web servers. The first web server is an ASP.NET Core WebAPI server. The second server is a Blazor WebAssembly server. The diagram visualizes how the HTTP requests from a web browser are sent to YARP. If the requested path starts with "/API/", the proxy forwards the request to the ASP.NET Core WebAPI server. Otherwise the request is sent to the Client Web Server which serves the Blazor WebAssembly application.

But why YARP instead of Apache or Nginx? YARP is built on top of ASP.NET Core which gives you some benefits:

  • You can configure YARP with any supported external configuration like JSON, environment variables, and many more. 
  • You can also use .NET code to configure and extend your reverse proxy.
  • You can store this piece of infrastructure in source control like any other ASP.NET Core project.
  • You can increase consistency between your local environment and server environments.
  • It's cross-platform, you can develop locally on your OS of choice but deploy it to your server OS of choice. For example, you can develop on Windows but use Linux machines for production.

Prerequisites #

To follow along you need the following tech:

  • .NET 6 (older versions work too with slight adjustments) (installation instructions)
  • OS which supports the above tech such as Windows, Mac, or Linux

You can find all the source code for the end result on GitHub.

Create the Blazor WASM client #

Before creating the client, create a folder where all projects will reside using the following commands:

mkdir YarpClientServerSingleOrigin
cd YarpClientServerSingleOrigin

Create a solution file using the dotnet command line interface (CLI):

dotnet new sln

Create the Blazor WASM client:

dotnet new blazorwasm -n Client -o Client

-n: specifies the name of the project

-o: specifies the location where the project will be created

Using the command above, you created a Blazor WASM project named 'Client' which is also used as the default namespace. This project is created in a subfolder also named 'Client'.

Add the client project to the solution file:

dotnet sln add Client

When you take a look at the FetchData component at Client\Pages\FetchData.razor, you can see the client is requesting data about the weather from a static JSON file:

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");

Update this line of code to fetch the data from api/WeatherForecast instead:

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/WeatherForecast");

This API endpoint does not exist yet, but you will create this soon.

In .NET 6, new web projects will use a random port number. To make sure you're using the same port as this tutorial, update the Client/Properties/launchSettings.json to match the content below:

{
  "profiles": {
    "Client": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "applicationUrl": "https://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

With this change, the web application will run on https://localhost:5000 when using dotnet run.

Create the ASP.NET Core Web API server #

Create the Web API project using the dotnet CLI:

dotnet new webapi -n Server -o Server

Using the command above, you created a Web API project named 'Server' which is also used as the default namespace. This project is created in a subfolder also named 'Server'.

Add the server project to the solution file:

dotnet sln add Server

To make sure you're using the same port as this tutorial, update the Server/Properties/launchSettings.json to match the content below:

{
  "profiles": {
    "Proxy": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

With this change, the web application will run on https://localhost:5001 when using dotnet run.

There is a single controller that comes out of the box with the WebAPI template: WeatherForecastController.cs.
This controller generates random weather forecasts and luckily for you matches the data model used in the Blazor WASM client. The WeatherForecast API can be requested at '/WeatherForecast', but the client will request it at '/api/WeatherForecast'. The reverse proxy you will create next will suffix the API with '/api'.

Create the YARP proxy #

Unlike some other reverse proxies, you cannot simply install it on your server and configure it. Instead, you have to create an empty ASP.NET Core Web project, add the YARP NuGet package, and configure YARP using configuration or code. Then you need to build, publish, and install it onto your server like any other ASP.NET Core application. This walkthrough will go through building and running this locally for development only.

Create an empty ASP.NET Core Web project using the dotnet CLI:

dotnet new web -n Proxy -o Proxy

Using the command above, you created an empty web project named 'Proxy' which is also used as the default namespace. This project is created in a subfolder also named 'Proxy'. In this case, Proxy is short for Reverse Proxy and not Forward Proxy.

Add the proxy project to the solution file:

dotnet sln add Proxy

To make sure you're using the same ports as this tutorial, update the Proxy/Properties/launchSettings.json to match the content below:

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "Server": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:5002",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

With this change, the proxy will run on https://localhost:5002 when using dotnet run.

Add the NuGet package for YARP to the proxy project:

dotnet add package Yarp.ReverseProxy

The package 'Yarp.ReverseProxy' is at version '1.0.0' at the time of writing this. If any of the upcoming functionality does not work as expected with newer versions of YARP, make sure to check out YARP's documentation.

Update the 'Program.cs' file to enable YARP's functionality:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

YARP also has documentation on how to add YARP to the older .NET templates.

You can use any external configuration like 'appsettings.json' or environment variables to configure YARP. You can also configure the reverse proxy dynamically using code.

Update the 'Proxy\appsettings.Development.json' file:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ReverseProxy": {
    "Clusters": {
      "Client": {
        "Destinations": {
          "Client1": {
            "Address": "https://localhost:5000"
          }
        }
      },
      "Server": {
        "Destinations": {
          "Server1": {
            "Address": "https://localhost:5001"
          }
        }
      }
    },
    "Routes": {
      "ServerRoute": {
        "ClusterId": "Server",
        "Match": {
          "Path": "/api/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/api"
          }
        ]
      },
      "ClientRoute": {
        "ClusterId": "Client",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    }
  }
}

The above JSON configures YARP as follows:

  • 2 clusters are defined
    • Client cluster only has one destination named "Client1" which points to the local development URL for the Blazor WASM client.
    • Server cluster only has one destination named "Server1" which points to the local development URL for the Web API server.
    1. A cluster named "Client"
    2. A cluster named "Server"
  • 3 routes are defined
    1. A route named "ServerRoute" forwards HTTP requests to the "Server" cluster any time the requested path starts with '/api/'. Before the HTTP request is forwarded to the Web API, the '/api' prefix is removed by the "PathRemovePrefix" transform.
    2. A route name "ClientRoute" forwards HTTP requests to the "Client" cluster for all remaining requests.

In this case, there is only one destination per cluster, but YARP can handle multiple destinations and the reverse proxy will load balance the HTTP requests.

Testing the result #

You need to open three different terminals. One for every project. Run the following commands:

  • Terminal 1: dotnet run --project Client
  • Terminal 2: dotnet run --project Server
  • Terminal 3: dotnet run --project Proxy

Open the browser and navigate to the Proxy which should be accessible at 'https://localhost:5002'.

The Blazor client should be showing up. Click on the "Fetch Data" navigation link.

The weather forecast data should populate just as you expect it from an out-of-the-box Blazor WASM project. But every time you reload, the data should be different because it is being generated by the Web API project.

You can write a script to bring up and down all three projects instead of manually running them in three different terminals. OR you could use Microsoft's experimental project Tye to tie everything together.

Install Tye as a .NET global tool:

dotnet tool install --global Microsoft.Tye --version 0.10.0-alpha.21420.1

You can find the most recent version of Tye at Nuget.

Initialize tye using this command:

tye init

This will create a configuration YAML file 'tye.yaml'. Update the YAML file to match the content below:

name: yarpclientserversingleorigin
services:
- name: client
  project: Client/Client.csproj
  bindings:
  - port: 5000
    protocol: https
- name: server
  project: Server/Server.csproj
  bindings:
  - port: 5001
    protocol: https
- name: proxy
  project: Proxy/Proxy.csproj
  bindings:
  - port: 5002
    protocol: https

Tye will automatically use random ports for projects if you do not specify them. The Proxy project expects the client and server project to be run on ports 5000 and 5001. Hence you need to explicitly configure which port and protocol these projects use in the YAML file.

Run this command to bring up all projects at once with tye:

tye run

You can find the proxy URL in the output, or you can navigate to tye's dashboard at localhost:8000.

Screenshot of the project Tye dashboard

In addition to finding the URL for each service, you can also watch the log streams of each service.

Summary #

API's need to provide CORS headers to explicitly allow ajax requests from web clients hosted on a different origin. Reverse proxies can merge the API origin and client origin onto a single origin. Using Microsoft's new reverse proxy "YARP", you configured the proxy to forward requests to '/api' to the Web API, and all other requests to the Blazor WASM client.
Using project Tye, you tied all projects together so you can easily run all projects at once instead of starting them in 3 different terminals.

You can learn more about YARP at YARP's Documentation and you can learn more about project Tye on its GitHub repository.

Related Posts

Related Posts