5 min read
Advanced Azure API Management Policies
Azure API Management (APIM) policies are powerful XML-based configurations that transform API behavior without modifying backend code. Today, I will explore advanced policy patterns that solve real-world integration challenges.
Understanding Policy Scopes
Policies can be applied at four scopes:
- Global: Applies to all APIs
- Product: Applies to APIs in a product
- API: Applies to all operations in an API
- Operation: Applies to a specific operation
<policies>
<inbound>
<!-- Policies applied before the request reaches the backend -->
</inbound>
<backend>
<!-- Policies applied when forwarding to backend -->
</backend>
<outbound>
<!-- Policies applied to the response -->
</outbound>
<on-error>
<!-- Policies applied when an error occurs -->
</on-error>
</policies>
Authentication and Authorization
JWT Validation with Claims
<policies>
<inbound>
<validate-jwt header-name="Authorization"
failed-validation-httpcode="401"
failed-validation-error-message="Unauthorized">
<openid-config url="https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration" />
<audiences>
<audience>api://my-api</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/{tenant}/</issuer>
</issuers>
<required-claims>
<claim name="roles" match="any">
<value>API.ReadWrite</value>
<value>API.Admin</value>
</claim>
</required-claims>
</validate-jwt>
<!-- Extract claims for downstream use -->
<set-variable name="userId" value="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Claims.GetValueOrDefault("oid", ""))" />
<set-variable name="userRoles" value="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Claims.GetValueOrDefault("roles", ""))" />
<!-- Pass user context to backend -->
<set-header name="X-User-Id" exists-action="override">
<value>@((string)context.Variables["userId"])</value>
</set-header>
</inbound>
</policies>
API Key with Subscription Tiers
<policies>
<inbound>
<!-- Validate subscription key -->
<check-header name="Ocp-Apim-Subscription-Key"
failed-check-httpcode="401"
failed-check-error-message="API key required" />
<!-- Get subscription tier from product -->
<set-variable name="subscriptionTier"
value="@(context.Subscription.ProductName)" />
<!-- Apply rate limits based on tier -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<string>("subscriptionTier") == "Free")">
<rate-limit calls="100" renewal-period="3600" />
<quota calls="1000" renewal-period="86400" />
</when>
<when condition="@(context.Variables.GetValueOrDefault<string>("subscriptionTier") == "Standard")">
<rate-limit calls="1000" renewal-period="3600" />
<quota calls="50000" renewal-period="86400" />
</when>
<when condition="@(context.Variables.GetValueOrDefault<string>("subscriptionTier") == "Premium")">
<rate-limit calls="10000" renewal-period="3600" />
<!-- No daily quota for premium -->
</when>
</choose>
</inbound>
</policies>
Request/Response Transformation
GraphQL to REST Translation
<policies>
<inbound>
<!-- Transform GraphQL query to REST call -->
<set-variable name="graphqlQuery" value="@(context.Request.Body.As<JObject>()["query"].ToString())" />
<choose>
<when condition="@(((string)context.Variables["graphqlQuery"]).Contains("getUser"))">
<!-- Extract user ID from GraphQL variables -->
<set-variable name="userId"
value="@(context.Request.Body.As<JObject>()["variables"]["id"].ToString())" />
<!-- Rewrite to REST endpoint -->
<rewrite-uri template="@("/api/users/" + context.Variables["userId"])" />
<set-method>GET</set-method>
<set-body>@("")</set-body>
</when>
<when condition="@(((string)context.Variables["graphqlQuery"]).Contains("listOrders"))">
<rewrite-uri template="/api/orders" />
<set-method>GET</set-method>
</when>
</choose>
</inbound>
<outbound>
<!-- Wrap REST response in GraphQL format -->
<set-body>@{
var response = context.Response.Body.As<JObject>();
return new JObject(
new JProperty("data", response),
new JProperty("errors", null)
).ToString();
}</set-body>
</outbound>
</policies>
Response Filtering Based on User Role
<policies>
<outbound>
<choose>
<when condition="@(!context.Variables.GetValueOrDefault<string>("userRoles", "").Contains("Admin"))">
<!-- Filter sensitive fields for non-admin users -->
<set-body>@{
var response = context.Response.Body.As<JObject>();
// Remove sensitive fields
var sensitiveFields = new[] { "ssn", "salary", "internalNotes", "costPrice" };
void RemoveSensitiveFields(JToken token)
{
if (token is JObject obj)
{
foreach (var field in sensitiveFields)
{
obj.Remove(field);
}
foreach (var child in obj.Children())
{
RemoveSensitiveFields(child);
}
}
else if (token is JArray arr)
{
foreach (var item in arr)
{
RemoveSensitiveFields(item);
}
}
}
RemoveSensitiveFields(response);
return response.ToString();
}</set-body>
</when>
</choose>
</outbound>
</policies>
Caching Strategies
Conditional Caching
<policies>
<inbound>
<!-- Only cache GET requests for authenticated users -->
<choose>
<when condition="@(context.Request.Method == "GET" && context.Variables.ContainsKey("userId"))">
<cache-lookup vary-by-developer="false"
vary-by-developer-groups="false"
downstream-caching-type="none">
<vary-by-header>Accept</vary-by-header>
<vary-by-query-parameter>page</vary-by-query-parameter>
<vary-by-query-parameter>pageSize</vary-by-query-parameter>
</cache-lookup>
</when>
</choose>
</inbound>
<outbound>
<choose>
<when condition="@(context.Request.Method == "GET" && context.Response.StatusCode == 200)">
<!-- Cache successful GET responses for 5 minutes -->
<cache-store duration="300" />
</when>
</choose>
</outbound>
</policies>
External Redis Cache
<policies>
<inbound>
<cache-lookup-value key="@("order-" + context.Request.MatchedParameters["orderId"])"
variable-name="cachedOrder"
caching-type="external" />
<choose>
<when condition="@(context.Variables.ContainsKey("cachedOrder"))">
<!-- Return cached response immediately -->
<return-response>
<set-status code="200" reason="OK" />
<set-header name="X-Cache" exists-action="override">
<value>HIT</value>
</set-header>
<set-body>@((string)context.Variables["cachedOrder"])</set-body>
</return-response>
</when>
</choose>
</inbound>
<outbound>
<choose>
<when condition="@(context.Response.StatusCode == 200)">
<cache-store-value key="@("order-" + context.Request.MatchedParameters["orderId"])"
value="@(context.Response.Body.As<string>(preserveContent: true))"
duration="600"
caching-type="external" />
<set-header name="X-Cache" exists-action="override">
<value>MISS</value>
</set-header>
</when>
</choose>
</outbound>
</policies>
Backend Integration Patterns
Circuit Breaker with Retry
<policies>
<backend>
<retry condition="@(context.Response.StatusCode >= 500)"
count="3"
interval="1"
max-interval="10"
delta="2"
first-fast-retry="true">
<forward-request buffer-request-body="true" timeout="30" />
</retry>
</backend>
<on-error>
<choose>
<!-- Circuit breaker: if backend fails repeatedly, return cached response -->
<when condition="@(context.LastError.Reason == "Timeout" || context.Response.StatusCode >= 500)">
<cache-lookup-value key="@("fallback-" + context.Request.Url.Path)"
variable-name="fallbackResponse" />
<choose>
<when condition="@(context.Variables.ContainsKey("fallbackResponse"))">
<return-response>
<set-status code="200" reason="OK (Cached)" />
<set-header name="X-Fallback" exists-action="override">
<value>true</value>
</set-header>
<set-body>@((string)context.Variables["fallbackResponse"])</set-body>
</return-response>
</when>
<otherwise>
<return-response>
<set-status code="503" reason="Service Temporarily Unavailable" />
<set-body>@{
return new JObject(
new JProperty("error", "Service temporarily unavailable"),
new JProperty("retryAfter", 30)
).ToString();
}</set-body>
</return-response>
</otherwise>
</choose>
</when>
</choose>
</on-error>
</policies>
Request Aggregation
<policies>
<inbound>
<base />
</inbound>
<backend>
<!-- Make parallel calls to multiple backends -->
<send-request mode="new" response-variable-name="userResponse" timeout="10" ignore-error="true">
<set-url>@("https://users-api.internal/api/users/" + context.Request.MatchedParameters["userId"])</set-url>
<set-method>GET</set-method>
</send-request>
<send-request mode="new" response-variable-name="ordersResponse" timeout="10" ignore-error="true">
<set-url>@("https://orders-api.internal/api/users/" + context.Request.MatchedParameters["userId"] + "/orders")</set-url>
<set-method>GET</set-method>
</send-request>
<send-request mode="new" response-variable-name="preferencesResponse" timeout="10" ignore-error="true">
<set-url>@("https://preferences-api.internal/api/users/" + context.Request.MatchedParameters["userId"] + "/preferences")</set-url>
<set-method>GET</set-method>
</send-request>
</backend>
<outbound>
<!-- Aggregate responses -->
<set-body>@{
var user = ((IResponse)context.Variables["userResponse"]).Body.As<JObject>();
var orders = ((IResponse)context.Variables["ordersResponse"]).Body.As<JArray>();
var prefs = ((IResponse)context.Variables["preferencesResponse"]).Body.As<JObject>();
return new JObject(
new JProperty("user", user),
new JProperty("recentOrders", orders),
new JProperty("preferences", prefs)
).ToString();
}</set-body>
</outbound>
</policies>
Logging and Monitoring
Structured Logging to Event Hub
<policies>
<inbound>
<set-variable name="requestId" value="@(Guid.NewGuid().ToString())" />
<set-variable name="requestTime" value="@(DateTime.UtcNow)" />
</inbound>
<outbound>
<log-to-eventhub logger-id="api-logger">@{
var requestTime = (DateTime)context.Variables["requestTime"];
var duration = (DateTime.UtcNow - requestTime).TotalMilliseconds;
return new JObject(
new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
new JProperty("requestId", context.Variables["requestId"]),
new JProperty("api", context.Api.Name),
new JProperty("operation", context.Operation.Name),
new JProperty("method", context.Request.Method),
new JProperty("url", context.Request.Url.ToString()),
new JProperty("statusCode", context.Response.StatusCode),
new JProperty("durationMs", duration),
new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
new JProperty("clientIp", context.Request.IpAddress),
new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", ""))
).ToString();
}</log-to-eventhub>
</outbound>
</policies>
Best Practices
- Use Policy Fragments: Extract reusable policies into fragments
- Test Incrementally: Use the APIM test console during development
- Monitor Performance: Policy execution adds latency; measure impact
- Handle Errors: Always implement on-error policies
- Version Policies: Store policies in source control
- Use Named Values: Externalize configuration for different environments
Azure API Management policies provide a powerful declarative way to implement cross-cutting concerns. Mastering policies enables you to build robust, secure, and performant API gateways without modifying backend services.