API Management Policies Deep Dive
Understanding the Policy Pipeline
Every API request in APIM passes through four policy sections:
<policies>
<inbound>
<!-- Applied before the request is forwarded to the backend -->
</inbound>
<backend>
<!-- Applied just before the request is forwarded -->
</backend>
<outbound>
<!-- Applied to the response before sending to the caller -->
</outbound>
<on-error>
<!-- Applied when an error occurs at any stage -->
</on-error>
</policies>
Essential Inbound Policies
Rate Limiting
<rate-limit calls="100" renewal-period="60" />
Limits callers to 100 calls per 60 seconds. Use rate-limit-by-key for per-user or per-IP limits.
IP Filtering
<ip-filter action="allow">
<address-range from="10.0.0.0" to="10.0.0.255" />
</ip-filter>
JWT Validation
<validate-jwt header-name="Authorization" require-scheme="Bearer">
<openid-config url="https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration" />
<required-claims>
<claim name="aud" match="all">
<value>{client-id}</value>
</claim>
</required-claims>
</validate-jwt>
CORS
<cors allow-credentials="true">
<allowed-origins>
<origin>https://example.com</origin>
</allowed-origins>
<allowed-methods>
<method>GET</method>
<method>POST</method>
</allowed-methods>
<allowed-headers>
<header>Authorization</header>
<header>Content-Type</header>
</allowed-headers>
</cors>
Outbound Policies
Response Transformation
<set-body>@{
var response = context.Response.Body.As<JObject>();
response["source"] = "api-gateway";
return response.ToString();
}</set-body>
Header Management
<set-header name="X-Request-Id" exists-action="skip">
<value>@(context.RequestId.ToString())</value>
</set-header>
Error Handling Policies
<on-error>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
return new JObject(
new JProperty("error", context.LastError.Message),
new JProperty("requestId", context.RequestId)
).ToString();
}</set-body>
</on-error>
Policy Expressions
Policy expressions are C# code snippets prefixed with @:
- Single expression:
@(context.Request.IpAddress) - Multi-statement:
@{ var x = 1; return x.ToString(); }
Backend Policies
Forward Request
<forward-request timeout="30" follow-redirects="true" buffer-request-body="true" />
Set Backend Service
<set-backend-service base-url="https://backend-v2.example.com" />
Send Request (Fan-Out Pattern)
Call multiple backends and aggregate results:
<inbound>
<send-request mode="new" response-variable-name="ordersResponse" timeout="20">
<set-url>https://orders-api.example.com/orders</set-url>
<set-method>GET</set-method>
</send-request>
<send-request mode="new" response-variable-name="customersResponse" timeout="20">
<set-url>https://customers-api.example.com/customers</set-url>
<set-method>GET</set-method>
</send-request>
<return-response>
<set-status code="200" reason="OK" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>@{
var orders = ((IResponse)context.Variables["ordersResponse"]).Body.As<JArray>();
var customers = ((IResponse)context.Variables["customersResponse"]).Body.As<JArray>();
return new JObject(
new JProperty("orders", orders),
new JProperty("customers", customers)
).ToString();
}</set-body>
</return-response>
</inbound>
Policy Fragments
Reusable policy blocks that can be referenced across multiple APIs:
<!-- Define a fragment -->
<fragment>
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription?.Key ?? context.Request.IpAddress)" />
<set-header name="X-Request-Id" exists-action="skip">
<value>@(context.RequestId.ToString())</value>
</set-header>
</fragment>
Reference in a policy:
<inbound>
<include-fragment fragment-id="standard-inbound" />
<!-- Additional policies -->
</inbound>
Microsoft Reference: Policy fragments
Caching Policies
Response Caching
<inbound>
<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>status</vary-by-query-parameter>
</cache-lookup>
</inbound>
<outbound>
<cache-store duration="3600" />
</outbound>
Cache Value (Custom Key-Value)
<cache-lookup-value key="@("customer-" + context.Request.MatchedParameters["id"])"
variable-name="cachedCustomer" />
<choose>
<when condition="@(context.Variables.ContainsKey("cachedCustomer"))">
<return-response>
<set-body>@((string)context.Variables["cachedCustomer"])</set-body>
</return-response>
</when>
</choose>
Transformation Policies
Rewrite URL
<rewrite-uri template="/api/v2/orders/{orderId}" />
Set Query Parameter
<set-query-parameter name="api-version" exists-action="override">
<value>2024-01-01</value>
</set-query-parameter>
Find and Replace in Body
<find-and-replace from="http://internal-api.local" to="https://api.example.com" />
JSON to XML Conversion
<outbound>
<choose>
<when condition="@(context.Request.Headers.GetValueOrDefault("Accept","").Contains("xml"))">
<json-to-xml apply="always" consider-accept-header="true" />
</when>
</choose>
</outbound>
Best Practices
- Apply policies at the appropriate scope — Global → Product → API → Operation (most specific wins)
- Use Named Values for configuration that changes between environments
- Test policies in the portal before deploying via CI/CD
- Use policy fragments to reuse common policy blocks across APIs
- Log errors with
<trace>policy during development - Use
<base />tag to inherit policies from parent scopes - Keep policy expressions simple — complex C# logic should be in the backend
- Cache aggressively for read-heavy APIs to reduce backend load
- Use
<send-request>for API composition and fan-out patterns - Version your policies in source control alongside API definitions