Modern digital applications rarely serve a single type of client. Web portals, mobile apps, partner integrations, and internal tools often consume the same backend services—yet each has different performance, payload, and UX requirements.
Exposing backend APIs directly to all clients frequently leads to over-fetching, chatty networks, and tight coupling between UI and backend domain models. This is where a Curated API or Backend for Frontend API design pattern becomes useful.
What Is the Backend-for-Frontend (BFF) Pattern?
The Backend-for-Frontend (BFF)—also known as the Curated API pattern—solves this problem by introducing a client-specific API layer that shapes, aggregates, and optimizes data specifically for the consuming experience. There is very good architectural guidance on this at Azure Architecture Center [Check out the 1st Link on Citation section]
The BFF pattern introduces a dedicated backend layer for each frontend experience. Instead of exposing generic backend services directly, the BFF:
- Aggregates data from multiple backend services
- Filters and reshapes responses
- Optimizes payloads for a specific client
- Shields clients from backend complexity and change
Each frontend (web, mobile, partner) can evolve independently, without forcing backend services to accommodate UI-specific concerns.
Why Azure API Management Is a Natural Fit for BFF
Azure API Management is commonly used as an API gateway, but its policy engine enables much more than routing and security.
Using APIM policies, you can:
- Call multiple backend services (sequentially or in parallel)
- Transform request and response payloads to provide a unform experience
- Apply caching, rate limiting, authentication, and resiliency policies
All of this can be achieved without modifying backend code, making APIM an excellent place to implement the BFF pattern.
When Should You Use a Curated API in APIM?
Using APIM as a BFF makes sense when:
- Frontend clients require optimized, experience-specific payloads
- Backend services must remain generic and reusable
- You want to reduce round trips from mobile or low-bandwidth clients
- You want to implement uniform polices for cross cutting concerns, authentication/authorization, caching, rate-limiting and logging, etc.
- You want to avoid building and operating a separate aggregation service
- You need strong governance, security, and observability at the API layer
How the BFF Pattern Works in Azure API Management
There is a Git Hub Repository [Check out the 2nd Link on Citation section] that provides a wealth of information and samples on how to create complex APIM policies.
I recently contributed to this repository with a sample policy for Curated APIs [Check out the 3rd Link on Citation section]
At a high level, the policy follows this flow:
- APIM receives a single client request
- APIM issues parallel calls to multiple backend services as shown below<wait for="all">
<send-request mode="copy" response-variable-name="operation1" timeout="{{bff-timeout}}" ignore-error="false">
<set-url>@("{{bff-baseurl}}/operation1?param1=" + context.Request.Url.Query.GetValueOrDefault("param1", "value1"))</set-url>
</send-request>
<send-request mode="copy" response-variable-name="operation2" timeout="{{bff-timeout}}" ignore-error="false">
<set-url>{{bff-baseurl}}/operation2</set-url>
</send-request>
<send-request mode="copy" response-variable-name="operation3" timeout="{{bff-timeout}}" ignore-error="false">
<set-url>{{bff-baseurl}}/operation3</set-url>
</send-request>
<send-request mode="copy" response-variable-name="operation4" timeout="{{bff-timeout}}" ignore-error="false">
<set-url>{{bff-baseurl}}/operation4</set-url>
</send-request>
</wait>
Few things to consider
- The Wait policy allows us to make multiple requests using nested send-request policies. The for="all" attribute value implies that the policy execution will await all the nested send requests before moving to the next one.
- {{bff-baseurl}}: This example assumes a single base URL for all end points. It does not have to be. The calls can be made to any endpoint
- response-variable-name attribute sets a unique variable name to hold response object from each of the parallel calls. This will be used later in the policy to transform and produce the curated result.
- timeout attribute: This example assumes uniform timeouts for each endpoint, but it might vary as well.
- ignore-error: set this to true only when you are not concerned about the response from the backend (like a fire and forget request) otherwise keep it false so that the response variable captures the response with error code.
- Once responses from all the requests have been received (or timed out) the policy execution moves to the next policy
- Then the responses from all requests are collected and transformed into a single response data<!-- Collect the complete response in a variable. --> <set-variable name="finalResponseData" value="@{ JObject finalResponse = new JObject(); int finalStatus = 200; // This assumes the final success status (If all backend calls succeed) is 200 - OK, can be customized. string finalStatusReason = "OK"; void ParseBody(JObject element, string propertyName, IResponse response){ string body = ""; if(response!=null){ body = response.Body.As<string>(); try{ var jsonBody = JToken.Parse(body); element.Add(propertyName, jsonBody); } catch(Exception ex){ element.Add(propertyName, body); } } else{ element.Add(propertyName, body); //Add empty body if the response was not captured } } JObject PrepareResponse(string responseVariableName){ JObject responseElement = new JObject(); responseElement.Add("operation", responseVariableName); IResponse response = context.Variables.GetValueOrDefault<IResponse>(responseVariableName); if(response == null){ finalStatus = 207; // if any of the responses are null; the final status will be 207 finalStatusReason = "Multi Status"; ParseBody(responseElement, "error", response); return responseElement; } int status = response.StatusCode; responseElement.Add("status", status); if(status == 200){ // This assumes all the backend APIs return 200, if they return other success responses (e.g. 201) add them here ParseBody(responseElement, "body", response); } else{ // if any of the response codes are non success, the final status will be 207 finalStatus = 207; finalStatusReason = "Multi Status"; ParseBody(responseElement, "error", response); } return responseElement; } // Gather responses into JSON Array // Pass on the each of the response variable names here. JArray finalResponseBody = new JArray(); finalResponseBody.Add(PrepareResponse("operation1")); finalResponseBody.Add(PrepareResponse("operation2")); finalResponseBody.Add(PrepareResponse("operation3")); finalResponseBody.Add(PrepareResponse("operation4")); // Populate finalResponse with aggregated body and status information finalResponse.Add("body", finalResponseBody); finalResponse.Add("status", finalStatus); finalResponse.Add("reason", finalStatusReason); return finalResponse; }" /> What this code does is prepare the response into a single JSON Object. using the help of the PrepareResponse function. The JSON not only collects the response body from each response variable, but it also captures the response codes and determines the final response code based on the individual response codes. For the purpose of his example, I have assumed all operations are GET operations and if all operations return 200 then the overall response is 200-OK, otherwise it is 206 -Partial Content. This can be customized to the actual scenario as needed.
- Once the final response variable is ready, then construct and return a single response based on the above calculation<!-- This shows how to return the final response code and body. Other response elements (e.g. outbound headers) can be curated and added here the same way -->
<return-response>
<set-status code="@((int)((JObject)context.Variables["finalResponseData"]).SelectToken("status"))" reason="@(((JObject)context.Variables["finalResponseData"]).SelectToken("reason").ToString())" />
<set-body>@(((JObject)context.Variables["finalResponseData"]).SelectToken("body").ToString(Newtonsoft.Json.Formatting.None))</set-body>
</return-response>
-
This effectively turns APIM into an experience-specific backend tailored to frontend needs.
When not to use APIM for BFF Implementation?
While this approach works well when you want to curate a few responses together and apply a unified set of policies, there are some cases where you might want to rethink this approach
- When the need for transformation is complex. Maintaining a lot of code in APIM is not fun. If the response transformation requires a lot of code that needs to be unit tested and code that might change over time, it might be better to sand up a curation service. Azure Functions and Azure Container Apps are well suited for this.
- When each backend endpoint requires very complex request transformation, then that also increases the amount of code, then that would also indicate a need for an independent curation service.
- If you are not already using APIM then this does not warrant adding one to your architecture just to implement BFF.
Conclusion
Using APIM is one of the many approaches you can use to create a BFF layer on top of your existing endpoint. Let me know your thoughts con the comments on what you think of this approach.








