Managing OpenAPI Specifications with Backend For Frontend and Swagger UI
ASP.NET Core web application development can span from entirely server-side rendered UIs to front-end single-page
applications dominating the user experience. More often than not, you’ll likely have some client side code that fetches
information from a backend resource, whether an endpoint or a multipurpose API. As the .NET Identity company, Duende
recommends securing these calls, but you may be asking, how?
Security recommendations have evolved with the popularity of JavaScript and the initial explosion of single-page
application frameworks. As of this post, the best current practices recommend developers secure their browser-based
application with Backend For Frontend (BFF) and adopt a “no tokens in the browser” policy. Realizing that a
browser client’s storage is a potential attack vector means malicious parties can target high-value resources such as
refresh and access tokens stored locally, leading to compromised security models.
In this post, we’ll briefly recap the BFF pattern and then proceed to a sample of revealing your OpenAPI specifications
to users either with Swagger’s UI or client SDK generators.
But first, let’s talk about BFF for folks who are first learning about the pattern.
What is Backend For Frontend (BFF)?
In a JavaScript-heavy user experience, the code executing on the client must make fetch requests to a remote resource.
The remote resources may include backend endpoints, remote APIs, or third-party services, with each resource requiring
authentication and authorization.
For developers, securing APIs relies on tokens supplied by some combination of OAuth and OpenID. From a client
perspective, tokens must be passed along to the resource to complete the request. But where do clients get these tokens?
Before BFF, after a successful authentication attempt, developers stored a user’s tokens in the client and managed them
with front-end code. Today, security experts classify the client and its storage facilities as high-risk locations. If a
user’s machine were compromised, an attacker could exfiltrate these tokens and use them in nefarious ways.
With BFF, a dedicated backend manages all tokens on the server. The client and server use HTTP-only cookies to create a
trusted connection. When a client requests a resource from an API endpoint, it must first go through the BFF, which
attaches tokens to requests before proxying them to their destination. By relying on Cross-Origin Resource Sharing
protections built into modern browsers, BFF can also protect against common attacks, such as Cross-Site Forgery.
The use of BFF has some implications for developing a web application. From the perspective of the JavaScript code that
runs in the browser, all APIs are compiled into a single unified API surface, whether you have one API or many. The most
beneficial implication is that frontend code no longer needs to manage tokens, and all requests are securely and
adequately authenticated, easing the burden on development teams.
Learn more about BFF in our documentation.
In the next section, we’ll examine how to combine the OpenAPI specifications generated by each API to improve the user
experience for clients who consume it.
The Architecture of a BFF-Powered Solution
We’ll build a BFF solution with one Javascript-powered client application and two remote APIs providing data for the
experience.
You can find
the source code for this article on GitHub.
The implementation details of each aren’t essential, but how we configure them to work together is. Each API will expose
an OpenAPI specification describing the available endpoints, including the HTTP method, expected request, and possible
responses. A Swagger UI will consume these specifications as part of the BFF host. JavaScript and TypeScript clients can
also use these specifications to generate typed contracts.
Before examining the code, let’s briefly discuss why OpenAPI is essential to development teams.
Why is OpenAPI part of a modern development solution?
OpenAPI (formerly known as Swagger) is a specification for describing HTTP APIs. It defines a standard format for
describing API endpoints, request formats, and response codes. OpenAPI is crucial because it allows developers to
understand and use APIs easily and facilitates the generation of documentation and code clients.
With the popularity of polyglot development, it’s become very popular to generate type-safe clients based on OpenAPI
specifications provided by the server. The benefits are clear: Compile-time safety and idiomaticity for all API calls.
Some teams will manually create their OpenAPI contracts and generate client and server contracts from them. In contrast,
others generate the OpenAPI contract from the server using tools
like Swagger Codegen
or Kiota. If your backend exposes a single (local) API built
using .NET, exposing an OpenAPI contract using ASP.NET Core built-in functionality is straightforward.
OpenAPI is a highly flexible solution for a complex world, and there are many reasons why OpenAPI matters to modern
developers. The outcomes can vary depending on the quality of the specifications provided and how consumers use them.
Working with ASP.NET Core APIs and OpenAPI
We’ll use .NET Aspire to coordinate all
the elements described in the previous section. That includes one client application and two backend APIs. Here is our
.NET Aspire App Host.
Csharp
using Microsoft.Extensions.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api1 = builder.AddProject<Projects.OpenApi_Api1>(Services.Api1.ToString());
var api2 = builder.AddProject<Projects.OpenApi_Api2>(Services.Api2.ToString());
var bff = builder.AddProject<Projects.OpenApi_Bff>(Services.Bff.ToString());
bff.WithReference(api1)
.WithReference(api2)
.WithReference(bff);
builder.Build().Run(); Let’s start by registering our two API endpoints in our Program of the BFF Host. Note that we’ll use .NET Aspire’s
service discovery to map named resources to their final destination so that URIs in C# may look strange at first glance.
Csharp
app.MapRemoteBffApiEndpoint("/api1", "https://api1")
.WithOptionalUserAccessToken();
app.MapRemoteBffApiEndpoint("/api2", "https://api2")
.WithOptionalUserAccessToken(); We now have two APIs we can call from JavaScript, which our client can access through the paths/api1 and /api2,
respectively. The C# registration marks the user access token as optional because the OpenAPI specification endpoint
allows anonymous access. In contrast, each target endpoint can still authorize and reject incoming requests.
These APIs also use the OpenAPI functionality to produce their respective specifications. In the API projects, we callAddOpenApi and register the OpenAPI services.
Csharp
public static TBuilder AddDefaultOpenApiConfig<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(options =>
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>()
);
return builder;
} The BearerSecuritySchemeTransformer in
our sample makes sure the requirement for bearer
token authentication is added to the OpenAPI specification for both APIs.
Along with the OpenAPI services, we must register the middleware in the ASP.NET Core pipeline that exposes our OpenAPI
endpoint.
Csharp
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}Now, back in our BFF host, we can register the Swagger UI and our new OpenAPI specification JSON files from each API.
Csharp
app.UseSwaggerUI(c =>
{
// Add all swagger endpoints for all APIs
c.SwaggerEndpoint("/api1/openapi/v1.json", "Api #1");
c.SwaggerEndpoint("/api2/openapi/v1.json", "Api #2");
}); We need one more line in our UseSwaggerUI method to satisfy the CSRF protection provided by BFF.
Csharp
// Inject a javascript function to add a CSRF header to all requests
c.UseRequestInterceptor("function(request){ request.headers['X-CSRF'] = '1';return request;}"); While most of our infrastructure is now connected, we must take additional steps to make everything work. In the next
section, we’ll see how to transform each specification so that endpoints map correctly through our BFF and match the
expected paths.
Note: Users must authenticate to access endpoints through the Swagger UI.
The final sample implements a JavaScript-based
login button injected into the Swagger UI. Forcing authentication can be a developer's implementation choice.
OpenAPI Specification Transformers for BFF Hosts
While we’ve taken some essential steps in our solution, right now, the swagger documents are direct proxies from the
server, which has several problems:
- Server URL: The server URLs in these documents represent the direct URL to the API. Clients shouldn’t access the
API directly but through the BFF. - Security: The APIs are protected using bearer tokens. However, the frontend shouldn’t send bearer tokens but
should send authentication cookies instead. - Local Path: The URL the frontend calls must include the local path prefix. Therefore, we must append the local
path to each URL.
So, we’ll have to modify the OpenAPI specifications. There are several ways to handle the transformation. We’ll
demonstrate how to do this using a Yarp transform, but you could also implement an endpoint directly that performs this
function.
The following code adds a Yarp transform to ALL requests by changing the default BffYarpTransformBuilder. We’ll check
if we’re proxying an OpenAPI document inside this transform and adjust accordingly.
Csharp
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using OpenApi.BffOpenApiDocumentParser;
using Yarp.ReverseProxy.Transforms;
namespace OpenApi.Bff;
/// <summary>
/// TransformOpenApiDocumentForBff the openapi document as it's being streamed.
/// </summary>
/// <param name="basePath"></param>
public class OpenApiResponseTransform(string basePath) : ResponseTransform
{
public override async ValueTask ApplyAsync(ResponseTransformContext context)
{
// Check if the request path matches /openapi/{document}.json / .yaml
if (ProxyingOpenApiDocument(context))
{
if (context.ProxyResponse == null)
// nothing to do if no response from the proxy
return;
var outputStream = context.HttpContext.Response.Body;
// This line is needed because we're going to modify the output stream.
// If we don't do this, it's going to send both the original and the modified stream.
context.SuppressResponseBody = true;
var openApiDocumentStream = await context.ProxyResponse.Content.ReadAsStreamAsync();
await OpenApiTransformer.TransformOpenApiDocumentForBff(openApiDocumentStream, outputStream,
Services.Bff.ActualUri(), basePath);
}
}
private bool ProxyingOpenApiDocument(ResponseTransformContext context)
{
return context.HttpContext.Request.Path.StartsWithSegments($"{basePath}/openapi", out var remainingPath) &&
remainingPath.HasValue &&
(remainingPath.Value.EndsWith(".json") || remainingPath.Value.EndsWith(".yaml"));
}
} Next, we’ll need to add this implementation to our services collection, which will now check all requests proxied
through the BFF and whether they are OpenAPI specifications.
Csharp
builder.Services.AddSingleton<BffYarpTransformBuilder>((path, c) =>
{
DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(path, c);
c.ResponseTransforms.Add(new OpenApiResponseTransform(path));
});Rerunning our BFF Host and navigating to the UI, we can see our API specifications in the Swagger UI dropdown.

This result is excellent, but what if we wanted a single specification that combines all APIs into one all-encompassing
specification?
Combining All OpenAI Specifications into One
To combine all OpenAPI specifications into a single one, we can write a customer combiner class that merges all known
OpenAPI specifications at runtime.
Csharp
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
namespace OpenApi.Bff.OpenApi
{
public class OpenApiDocumentCombiner(HttpClient client, IOptions<OpenApiDocumentCombinerOptions> o)
{
private static readonly OpenApiStreamReader OpenApiStreamReader = new();
public async Task<FileStreamHttpResult> CombineDocuments(CancellationToken cancellationToken)
{
var doc = new OpenApiDocument();
if (o.Value.ServerUri != null)
{
doc.Servers.Add(new OpenApiServer
{
Url = o.Value.ServerUri.ToString()
});
}
doc.Paths = new OpenApiPaths();
doc.Components = new OpenApiComponents();
foreach (var source in o.Value.Documents)
{
var stream = await client.GetStreamAsync(source.DocumentUri, cancellationToken);
var docToMerge = OpenApiStreamReader.Read(stream, out _);
foreach (var path in docToMerge.Paths ?? [])
{
doc.Paths[source.LocalPath + path.Key] = path.Value;
}
foreach (var schema in docToMerge.Components.Schemas)
{
doc.Components.Schemas[schema.Key] = schema.Value;
}
foreach (var response in docToMerge.Components.Responses)
{
doc.Components.Responses[response.Key] = response.Value;
}
foreach (var parameter in docToMerge.Components.Parameters)
{
doc.Components.Parameters[parameter.Key] = parameter.Value;
}
foreach (var example in docToMerge.Components.Examples)
{
doc.Components.Examples[example.Key] = example.Value;
}
foreach (var requestBody in docToMerge.Components.RequestBodies)
{
doc.Components.RequestBodies[requestBody.Key] = requestBody.Value;
}
foreach (var header in docToMerge.Components.Headers)
{
doc.Components.Headers[header.Key] = header.Value;
}
//// We intentionally don't copy the security schemes.
//foreach (var securityScheme in docToMerge.Components.SecuritySchemes)
//{
// doc.Components.SecuritySchemes[securityScheme.Key] = securityScheme.Value;
//}
foreach (var link in docToMerge.Components.Links)
{
doc.Components.Links[link.Key] = link.Value;
}
foreach (var callback in docToMerge.Components.Callbacks)
{
doc.Components.Callbacks[callback.Key] = callback.Value;
}
}
var memoryStream = new MemoryStream();
doc.Serialize(memoryStream, OpenApiSpecVersion.OpenApi3_0, OpenApiFormat.Json);
memoryStream.Position = 0;
return TypedResults.Stream(memoryStream);
}
}
} In this class, you also have an opportunity to transform, change, or remove any sections not applicable to the final
result. In our case, we remove the security schemes since the BFF host will handle security for the solution.
We must now register our new specification endpoint and modify our Swagger UI registration. Let’s start with our
endpoint.
Csharp
app.MapGet("/swagger/combined/v1.json",
async (OpenApiDocumentCombiner c, CancellationToken ct) => await c.CombineDocuments(ct));Finally, let’s update the Swagger UI registration with our newly combined specifications.
Csharp
app.UseSwaggerUI(c =>
{
// Inject a javascript function to add a CSRF header to all requests
c.UseRequestInterceptor("function(request){ request.headers['X-CSRF'] = '1';return request;}");
// Add all swagger endpoints for all APIs
c.SwaggerEndpoint("/api1/openapi/v1.json", "API #1");
c.SwaggerEndpoint("/api2/openapi/v1.json", "API #2");
c.SwaggerEndpoint("/swagger/combined/v1.json", "Combined");
});When we run our solution now, we can select a “Combined” specification in our Swagger UI.

Most importantly, when we use the APIs from our client application, it all still works.

We can call our endpoints securely like we always could with BFF while allowing developers to explore the API surface we
provide. It couldn’t be easier.
Conclusion
In this article, adapted from our BFF sample, we
showed how you can take multiple approaches to exposing your OpenAPI specifications through a BFF host. We could
transform specifications by modifying endpoint definitions and combining all into a single-use specification.
OpenAPI can help teams better document and share APIs, whether it’s for developers or automated client generation. By
using the functionality of the BFF, we can build any solution we can imagine while still adhering to modern and current
security practices.
We hope you enjoyed this post. We recommend you check out
the entire sample, which shows all the elements
combined with a few additional implementation details left out for brevity. As always, we’d love to hear from you, so
feel free to leave a comment below!

