The Backend for Frontend (BFF) pattern gives each client type (mobile, web, admin) its own backend service that aggregates and optimises data from downstream APIs. It solves the problem of one API trying to serve clients with fundamentally different needs. This guide covers when BFF is worth the overhead, the BFF-for-SPA security pattern, how to build it on .NET and Azure, the anti-patterns that cause the most damage, and when simpler alternatives are better. Our API and integration services cover BFF design and implementation as part of broader API programmes.
The problem BFF solves
Most applications start with a single API that serves all clients. Early on, this works. The web app and the mobile app need roughly the same data, and one set of endpoints handles both.
The problems emerge as clients diverge:
- The mobile app needs smaller payloads. It fetches the same endpoints as the web app but discards half the fields. Bandwidth is wasted, and the app feels slower than it should.
- The web dashboard needs aggregated data. A single screen requires data from three different microservices. The front-end makes three calls and stitches them together, adding latency and complexity to the client.
- The internal admin tool needs richer access. Admin users need fields and operations that should never be exposed to external clients. The API accumulates permission checks and conditional logic.
- Versioning becomes painful. A change that benefits the web app breaks the mobile app. Every API change requires coordination across all client teams.
The single API becomes a bottleneck: too fat for mobile, too thin for dashboards, too permissive for security, and too coupled for independent releases.
The pattern that solves this is not new. It emerged at SoundCloud around 2011 and was named and documented by Sam Newman in Backends For Frontends. Microsoft now documents it formally in the Azure Architecture Center, which is worth reading alongside this guide for the Well-Architected Framework trade-offs.
What BFF looks like in practice
A BFF architecture introduces a thin service layer between each client type and the downstream services.
Mobile App ──▶ Mobile BFF ──▶ ┌─ User Service
├─ Order Service
Web App ──▶ Web BFF ──▶ ├─ Product Service
└─ Analytics Service
Admin Tool ──▶ Admin BFF ──▶
Each BFF:
- Aggregates data from multiple downstream services into a single response tailored to its client
- Transforms payloads to match the client’s needs (fewer fields for mobile, richer data for admin)
- Owns its own API contract and can evolve independently of other BFFs
- Handles client-specific auth flows (PKCE for mobile, session cookies for web, service tokens for admin)
The downstream services remain generic and reusable. The client-specific logic lives in the BFF, not in the downstream services or the front end.
There is a second, quite different reason to build a BFF, and it has nothing to do with payload shape. For browser applications, a BFF is the mechanism that keeps OAuth tokens out of JavaScript entirely. That security case is covered in its own section below, and it is often the stronger justification of the two.
BFF vs single API vs API gateway
These three patterns solve different problems and often coexist. The table below summarises where each one fits.
| Concern | Single API | API gateway | BFF |
|---|---|---|---|
| Client-specific payload shaping | No | No | Yes |
| Cross-service aggregation | Possible, pollutes the model | No | Yes |
| Authentication and token validation | Yes | Yes | Yes, per client type |
| Rate limiting and quotas | Application code | Yes | No, belongs at the gateway |
| Routing and versioning | Application code | Yes | No, belongs at the gateway |
| Independent client team release cadence | No | No | Yes |
| Keeps OAuth tokens out of the browser | No | No | Yes |
| Operational cost | Lowest | Low | Highest |
| Sensible starting point | Yes | Once you have more than one backend | Only when a real constraint appears |
Single API
One API serves all clients. This is the right starting point for most projects and remains the right architecture when:
- All clients need the same data in the same shape
- The API surface is small (under roughly 30 endpoints)
- There is one client team or the client teams are tightly coordinated
- Payload optimisation is not a priority
When it breaks down: when client requirements diverge enough that the API accumulates client-specific logic, conditional fields, and endpoint sprawl.
API gateway
A gateway like Azure API Management handles cross-cutting concerns: routing, authentication, rate limiting, and monitoring. It does not contain business logic.
The gateway works well with both single-API and BFF architectures:
- Single API and gateway: the gateway fronts one backend
- BFF and gateway: the gateway fronts multiple BFFs, routing by client type or API version
The gateway is not a replacement for BFF. It operates at a different layer. Microsoft’s guidance reinforces this split: cross-cutting features such as monitoring and authorisation should be abstracted out of the BFF and handled by the gateway using the Gatekeeper, Rate Limiting, and Gateway Routing patterns.
BFF
BFF adds a per-client service layer that contains client-specific aggregation and transformation logic. It makes sense when:
- Different client types have genuinely different data and performance requirements
- Client teams want to iterate independently on their API contracts
- You need different auth patterns per client type
- Payload optimisation matters (mobile bandwidth, admin tool data richness)
- A browser client needs OAuth tokens kept server-side
The cost: more services to build, deploy, and maintain. Only adopt BFF when the complexity of a single API exceeds the complexity of multiple BFF services.
The BFF-for-SPA security pattern
This is the part of the pattern most often skipped, and for a single-page application it is usually the main event.
The problem with tokens in the browser
A single-page application that performs its own authorization code flow ends up holding an access token, and often a refresh token, somewhere the browser can reach. That means localStorage, sessionStorage, or a JavaScript variable. All three are readable by any script running on the page.
This matters because a single cross-site scripting (XSS) flaw stops being a session problem and becomes a token theft problem. An attacker who can run script in your page can exfiltrate the token and use it directly against your API, from their own infrastructure, for as long as the token lives. Refresh token rotation reduces the window but does not close it.
The IETF’s OAuth 2.0 for Browser-Based Applications working document, which is progressing as a Best Current Practice, works through these threats in detail. Its central architectural recommendation is the token-mediating backend: a server-side component that holds the tokens on the browser’s behalf. That component is a BFF.
How the pattern works
The flow is straightforward once the responsibility moves server-side:
- The browser hits a login route on the BFF. The BFF, not the SPA, starts the OpenID Connect authorization code flow with Proof Key for Code Exchange, defined in RFC 7636.
- The identity provider redirects back to the BFF with an authorization code. The BFF exchanges it for tokens using its client secret, which never leaves the server.
- The BFF stores the access and refresh tokens server-side, keyed to a session, and issues the browser an encrypted,
HttpOnly,Secure,SameSitecookie. - The SPA calls its API through the BFF on the same origin. The BFF looks up the session, attaches the access token, and proxies the call to the downstream API.
- Token refresh happens on the server. The SPA never sees, stores, or handles a token.
The browser holds a cookie it cannot read. An XSS flaw can still make requests as the user while the page is open, which is bad, but the attacker cannot lift a bearer token and replay it elsewhere. That is a meaningful reduction in blast radius.
Cookies, CSRF, and same-origin
Moving to cookies reintroduces cross-site request forgery (CSRF) as a concern, so the pattern layers three defences:
SameSite=Strictcookies, so the browser will not attach the session cookie to cross-site requests- A required custom header on every API call, which a cross-site form post cannot set
- A restrictive CORS policy, so only allowed origins can make credentialed requests
Duende’s BFF Security Framework implements exactly this combination, and requires a custom x-csrf header on API endpoints by default. Prefixing the cookie name with __Host- is worth doing as well, because it forces the cookie to be secure, host-only, and path-scoped to the root.
Building it on .NET
Two libraries do most of the work, and they compose.
Duende.BFF is the purpose-built option. It packages session management, the OIDC interactions, CSRF protection, and API proxying for React, Angular, Vue, and Blazor WebAssembly front ends. The registration follows the standard ASP.NET Core authentication pattern:
builder.Services.AddBff()
.ConfigureOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code";
// offline_access is what makes server-side refresh possible.
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
})
.ConfigureCookies(options =>
{
// __Host- forces Secure, host-only, and path=/.
options.Cookie.Name = "__Host-bff";
options.Cookie.SameSite = SameSiteMode.Strict;
});
Note the licensing before you commit: Duende.BFF is free for development, testing, and personal projects, but production use requires a paid licence. Budget for it at design time rather than at go-live.
YARP, Microsoft’s reverse proxy library for ASP.NET Core, is the other half. It handles the proxying of front-end calls to downstream APIs with full programmatic control over routing and request transformation, inside the ASP.NET Core pipeline. Duende.BFF integrates with YARP directly, and YARP on its own is a reasonable choice if your BFF is mostly pass-through and you would rather not take a licence dependency.
If you are running the wider system on .NET Aspire, both fit its service defaults and service discovery without special handling.
Implementing BFF on Azure
Architecture
A typical BFF deployment on Azure:
- Azure API Management as the unified gateway, routing to BFF backends by path prefix or custom header
- BFF services on Azure App Service (simple), Azure Container Apps (autoscaling and container flexibility), or Azure Functions (the option Microsoft’s own reference example uses)
- Downstream services on App Service, AKS, or Azure Functions
- Azure Entra ID for authentication, with each BFF validating tokens appropriate to its client type
- Azure Cache for Redis for BFF session state, which is required as soon as you run more than one instance
Routing strategy
APIM routes traffic to the correct BFF based on a path prefix:
/api/mobile/*routes to the mobile BFF/api/web/*routes to the web BFF/api/admin/*routes to the admin BFF
Alternatively, use a custom X-Client-Type header. The path prefix approach is simpler and more transparent for debugging.
Auth per client type
Each BFF handles the auth flow appropriate to its client:
- Mobile BFF: OAuth 2.0 with PKCE, refresh tokens, biometric-triggered re-auth. Native apps can hold tokens in platform-secured storage, so the token-mediating pattern is less critical here than it is in a browser. See our mobile app backends service for the full pattern.
- Web BFF: the token-mediating pattern described above, with server-side sessions and a hardened cookie.
- Admin BFF: Azure Entra ID with role-based access control (RBAC) and conditional access policies.
APIM validates the token at the gateway layer. The BFF validates claims and applies client-specific authorisation logic. Do not let the BFF trust a client-supplied user identifier just because the request arrived through the gateway.
A worked aggregation endpoint
This is what the aggregation half of a BFF actually looks like. The screen needs an order, its customer, and its delivery tracking. Three downstream calls become one client call, the two independent calls run concurrently, and a failure in the non-critical service degrades rather than breaking the screen.
// Mobile BFF: one call backs the whole order summary screen.
app.MapGet("/orders/{id}/summary", async (
string id,
IOrderService orders,
ICustomerService customers,
IDeliveryService delivery,
ILogger<Program> log,
CancellationToken ct) =>
{
var order = await orders.GetAsync(id, ct);
if (order is null)
{
// RFC 9457 problem details, not a raw downstream error.
return Results.Problem(
title: "Order not found",
statusCode: StatusCodes.Status404NotFound);
}
// Independent calls run concurrently. Serial awaits here would add
// the two latencies together for no reason.
var customerTask = customers.GetAsync(order.CustomerId, ct);
var trackingTask = delivery.GetTrackingAsync(id, ct);
await Task.WhenAll(customerTask, trackingTask);
return Results.Ok(new OrderSummary(
order.Id,
order.Total,
customerTask.Result.DisplayName,
// Tracking is decoration, not the point of the screen.
// A delivery outage should not 500 the order summary.
trackingTask.Result?.EstimatedArrival));
});
Three things in that example are the actual work, and they are what separates a BFF from a passthrough:
- Concurrency. Independent downstream calls should overlap. Awaiting them in sequence is the most common performance mistake in aggregation code, and it silently makes the BFF slower than the client-side stitching it replaced.
- Graceful degradation. Decide per field whether a downstream failure is fatal to the response. The BFF is the right place to make that call, because only the BFF knows what the screen needs.
- Error translation. Downstream errors get translated into a consistent contract. RFC 9457 problem details is the standard worth adopting, and it replaced the older RFC 7807.
Add a timeout and a circuit breaker per downstream dependency. A BFF that waits indefinitely on a slow service turns one degraded dependency into a fully degraded client.
Caching
BFFs are excellent caching points. Each BFF knows exactly what its client needs and how often that data changes:
- Mobile BFF: aggressive caching with ETags and short time-to-live values for frequently changing data, long values for reference data
- Web BFF: cache aggregated dashboard responses with a lifetime matched to the reporting refresh interval
- Admin BFF: minimal caching, because admins usually need fresh data
Use Azure Cache for Redis or in-memory caching in the BFF process, depending on scale. Be careful with per-user data: a cache key that omits the user identity is a data leak, not a performance optimisation.
When to use GraphQL instead of BFF
GraphQL and BFF solve overlapping problems. Both address the “different clients need different data shapes” challenge, and Microsoft’s pattern documentation now says so directly: if you use GraphQL with frontend-specific resolvers, BFF services may add no value.
GraphQL as BFF replacement: a single GraphQL API lets each client query exactly the fields it needs. The mobile app requests a compact field set; the web dashboard requests a richer set. No separate BFF services needed.
When GraphQL works better than BFF:
- The data model is graph-shaped, with entities in deep relationships
- Client teams want maximum flexibility to change queries without backend changes
- You have the team skills to manage schema design, resolver performance, and query cost control
When BFF works better than GraphQL:
- Different clients need fundamentally different auth flows, not just different data shapes
- You need OAuth tokens kept out of the browser, which GraphQL does nothing to address
- Some clients need non-query operations (file uploads, streaming, WebSocket connections) that GraphQL handles awkwardly
- The team is more comfortable with REST-style services than GraphQL schema management
- You need strict control over what data each client type can access, which BFF makes explicit per service
The two also combine. A GraphQL server can sit inside a BFF, giving you the token-mediating security properties and flexible querying together. For a deeper comparison of API styles, see our guide on REST vs GraphQL vs gRPC.
BFF anti-patterns
These are the failure modes worth naming, because each one is easy to walk into and expensive to walk back out of.
Business logic in the BFF
The BFF should aggregate, transform, and optimise. If it is calculating prices, enforcing entitlement rules, or managing state, that logic belongs in a downstream service where every client can reach it.
The damage is not theoretical. Once pricing logic lives in the mobile BFF, the web BFF grows its own copy, and the two drift. You then have two answers to the same business question and no authoritative one. Microsoft’s guidance flags code duplication as a probable outcome of this pattern, and this is the form of it that actually hurts.
One BFF per application rather than per client type
If your iOS and Android apps have identical data needs, they share one mobile BFF. Splitting by application rather than by client type multiplies your operational surface without giving you anything.
The test is simple: if two clients would consume an identical contract, they do not need separate BFFs. Microsoft’s guidance is that the pattern may not suit you at all when interfaces make the same or similar requests.
The distributed monolith
This is the worst outcome. Every BFF is coupled tightly enough to the downstream services that a change to one service requires a coordinated release of all the BFFs.
You now have the operational cost of many services and the release coupling of one. The usual cause is BFFs reaching into downstream data models rather than consuming a stable contract. If your release plan involves the phrase “deploy all of them together”, you have built this.
Treating the BFF as a security boundary while trusting the client
A BFF is a good place to enforce authorisation, and a bad place to assume the caller is honest. If the BFF reads a user identifier from the request body or a header rather than from the validated token, an attacker will use it to read someone else’s data.
The identity used for downstream calls should come from validated token claims, every time, with no exceptions for internal tooling.
Adopting BFF too early
The most common one. Start with a single API. Introduce BFFs when client requirements genuinely diverge, when the browser token problem needs solving, or when the single API is visibly accumulating client-specific complexity.
Three BFFs means three deployment pipelines, three sets of alerts, three on-call surfaces, and one extra network hop on every request. Microsoft’s guidance calls out that added latency explicitly. If you cannot name the constraint the BFF removes, you do not need one yet.
Inconsistent error handling
Each BFF should return errors in a consistent format, and RFC 9457 problem details is the right default. Downstream service errors should be translated, not leaked through to the client as raw internal errors. Leaking them couples your client to your internal topology and hands an attacker a free map of your services.
Where to start
If you are considering BFF for your architecture:
- Work out which problem you are solving. Payload and aggregation pressure, and browser token security, are different problems with the same solution. The second one justifies a BFF on its own, even for a single client.
- Document where your current API serves clients differently. Look for conditional logic, client-specific query parameters, and endpoints used by only one client type. These are the pressure points where a single API is stretching.
- Evaluate whether GraphQL could solve the data-shape problem. If the issue is primarily flexibility and all clients share the same auth model, GraphQL may be simpler than multiple BFF services. It will not solve the token problem.
- Start with one BFF for the client type that differs most. Usually this is the browser SPA (token security) or mobile (smaller payloads, different auth flow, offline support). Keep the other clients on the single API until they need their own BFF.
- Set the operational baseline before the second BFF. Distributed tracing across the gateway, BFF, and downstream services, plus per-dependency timeouts and circuit breakers. Retrofitting this across three BFFs is considerably harder than building it into the first one.
For help designing a BFF architecture or evaluating whether it is the right fit, see our mobile app backends and API and integration services or book a consultation.
Frequently asked questions
What is the Backend for Frontend pattern?
How is BFF different from an API gateway?
When should I not use BFF?
How many BFFs should I have?
What is a BFF anti-pattern?
What is the BFF pattern for single-page applications?
Can I use GraphQL as a BFF?
How does BFF work with Azure API Management?
Related guides
Mobile API Best Practices: Building Backends That Scale
How to design, secure, and operate APIs for mobile apps. Protocol choice, authentication flows, offline sync, push notifications, versioning, and Azure architecture patterns.
REST vs GraphQL vs gRPC: Choosing the Right API Style for Enterprise Systems
A practical comparison of REST, GraphQL, gRPC, and tRPC for enterprise teams: when each style fits, the trade-offs, and a decision matrix.
Extending a Ferry Reservation System: An API Layer Architecture
How to build custom apps, disruption tools, and analytics on top of a ferry reservation platform using an operator-owned API layer, without touching the core.