Scenario

All our requests towards our API goes through a BFF (think of it as a proxy).
The client calls our BFF and the BFF proxies that call to our API.
Now, our API knows nothing about the BFF, it just receives a request and handles it accordingly.

To call our API, we simply call the BFF and prefix the path like this:

https://my-bff.local/my-api/orders/{orderId}

The problem

Our API is a HATEOAS API so it contains a bunch of links to different resources in its response, allowing the consumer to navigate appropriately without needing to hardcode any URLs.

Here's an example response:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://my-api.local/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://my-api.local/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://my-api.local/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://my-api.local/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://my-api.local/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://my-api.local/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

As you can see, the API returns absolute URLs pointing directly to the API.

That's a bit problematic.

Remember that we said that we didn't want the client to do any hardcoding or manipulation of the links? All the calls should go through the BFF. So, wouldn't it be nice if the links returned from the API pointed to the BFF instead of the API?

The solution

The link generation in the API is done like this:

public class HateoasLinkGenerator : ILinkGenerator
{
    private readonly LinkGenerator _linkGenerator;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AspNetLinkGenerator(LinkGenerator linkGenerator, IHttpContextAccessor httpContextAccessor)
    {
        _linkGenerator = linkGenerator;
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetUriByAction(string action, string controller, object? values = null)
    {
        var httpContext = _httpContextAccessor.HttpContext!;
        var result = _linkGenerator.GetUriByAction(
                httpContext!, action, controller, values) ??
            throw new Exception(
                $"Failed to generate link: '{action}' and controller '{controller}' returned null");
        return result;
    }
}

We don't want the API to know anything about the BFF, so we can't just replace the host and prefix the path and be done with it. Instead, we will use a couple of X-Forwarded headers.

The built-in LinkGenerator respects the usage of those headers.

X-Forwarded-Host

To return the correct host, we will send the X-Forwarded-Host header with the value of the BFF:

X-Forwarded-Host: my-bff.local

If we generate the response now, it will look like this:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://my-bff.local/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://my-bff.local/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://my-bff.local/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

As you can see, the links now point to the BFF, but they're still missing the my-api prefix.

X-Forwaded-Prefix

To append the my-api prefix to all the paths, we'll send the X-Forwarded-Prefix header from the BFF to the API like this:

X-Forwarded-Prefix: /my-api

Now, the response will look like this:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://my-bff.local/my-api/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://my-bff.local/my-api/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://my-bff.local/my-api/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/my-api/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/my-api/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://my-bff.local/my-api/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

Mission accomplished!

Don't forget to enable the forwarded headers middleware in your API if you're using ASP.NET Core.