Azure API Management Policy Engine
Modifying Request/Response Behavior Without Code Changes
Introduction
The Azure API Management (APIM) policy engine is a powerful feature that allows you to modify how API requests and responses are handled without changing any backend code. Policies are XML-based statements that execute sequentially on incoming requests and outgoing responses.
This comprehensive guide covers:
- Policy structure — Understanding the four policy sections
- Common policies — Frequently used policy statements
- Policy expressions — Using C# expressions in policies
- Advanced patterns — Combining policies for complex scenarios
- Best practices — Writing maintainable policies
Policy Execution Flow
Four Policy Sections
┌─────────────────────────────────────────────────────────────────────┐
│ POLICY EXECUTION FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ INBOUND │ │
│ │ • Transform request │ │
│ │ • Validate authentication │ │
│ │ • Rate limiting │ │
│ │ • Set headers/variables │ │
│ └────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ BACKEND │ │
│ │ • Modify request to backend │ │
│ │ • Route to different backend │ │
│ │ • Mock response │ │
│ └────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ OUTBOUND │ │
│ │ • Transform response │ │
│ │ • Add CORS headers │ │
│ │ • Cache response │ │
│ │ • Set headers │ │
│ └────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ON-ERROR │ │
│ │ • Handle errors │ │
│ │ • Log errors │ │
│ │ • Return custom error response │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Policy Structure
Basic Policy Template
<policies>
<inbound>
<!-- Policies executed before request is forwarded to backend -->
<base />
</inbound>
<backend>
<!-- Policies executed when forwarding request to backend -->
<base />
</backend>
<outbound>
<!-- Policies executed after response is received from backend -->
<base />
</outbound>
<on-error>
<!-- Policies executed when an error occurs -->
<base />
</on-error>
</policies>
Scope Levels
<!-- Global (All APIs) -->
<policies>
<inbound>...</inbound>
</policies>
<!-- API Scope -->
<api name="orders-api">
<inbound>...</inbound>
</api>
<!-- Operation Scope -->
<operation name="GetOrder">
<inbound>...</inbound>
</operation>
<!-- Product Scope -->
<product name="premium">
<inbound>...</inbound>
</product>
Common Inbound Policies
Set Request Header
<inbound>
<!-- Add or modify a header -->
<set-header name="X-Correlation-Id" exists-action="override">
<value>@(context.Request.Id)</value>
</set-header>
<!-- Add header if not exists -->
<set-header name="X-Request-Source" exists-action="skip">
<value>APIM</value>
</set-header>
<!-- Remove header -->
<set-header name="X-Remove-Me" exists-action="delete" />
</inbound>
Set Query Parameter
<inbound>
<!-- Add default parameter -->
<set-query-parameter name="api-version" exists-action="override">
<value>2023-01-01</value>
</set-query-parameter>
<!-- Optional parameter -->
<set-query-parameter name="page" exists-action="optional">
<value>1</value>
</set-query-parameter>
</inbound>
Set Variable
<inbound>
<!-- Store value in variable -->
<set-variable name="correlationId" value="@(Guid.NewGuid().ToString())" />
<!-- Extract from header -->
<set-variable name="userId" value="@(context.Request.Headers.GetValueOrDefault("X-User-Id", ""))" />
<!-- Parse JWT -->
<set-variable name="jwtClaims" value="@(context.Request.Headers.GetValueOrDefault("Authorization", "").Split(' ')[1])" />
</inbound>
Conditional Processing (Choose)
<inbound>
<choose>
<when condition="@(context.Request.Method == HttpMethod.Get)">
<set-header name="X-Request-Type" exists-action="override">
<value>Read</value>
</set-header>
</when>
<when condition="@(context.Request.Method == HttpMethod.Post)">
<set-header name="X-Request-Type" exists-action="override">
<value>Create</value>
</set-header>
</when>
<otherwise>
<set-header name="X-Request-Type" exists-action="override">
<value>Other</value>
</set-header>
</otherwise>
</choose>
</inbound>
Common Backend Policies
Route to Different Backend
<backend>
<!-- Route based on API version -->
<choose>
<when condition="@(context.Request.Url.Query.GetValueOrDefault("version") == "v2")">
<set-backend-service base-url="https://api-v2.example.com" />
</when>
<otherwise>
<set-backend-service base-url="https://api-v1.example.com" />
</otherwise>
</choose>
</backend>
Mock Response (for testing)
<backend>
<!-- Return mock response without calling backend -->
<mock-response status-code="200" content-type="application/json">
<set-body>{
"status": "success",
"data": {
"message": "This is a mock response"
}
}</set-body>
</mock-response>
</backend>
Common Outbound Policies
Transform JSON Response
<outbound>
<!-- Rename properties in response -->
<json-transformer>
<rename-json-element name="oldName" new-name="newName" />
</json-transformer>
</outbound>
Add CORS Headers
<outbound>
<set-header name="Access-Control-Allow-Origin" exists-action="override">
<value>*</value>
</set-header>
<set-header name="Access-Control-Allow-Methods" exists-action="override">
<value>GET, POST, PUT, DELETE, OPTIONS</value>
</set-header>
<set-header name="Access-Control-Allow-Headers" exists-action="override">
<value>Content-Type, Authorization</value>
</set-header>
</outbound>
Set Response Header
<outbound>
<set-header name="X-Response-Time" exists-action="override">
<value>@(DateTime.UtcNow.ToString("o"))</value>
</set-header>
</outbound>
Cache Response
<outbound>
<!-- Check cache first -->
<cache-lookup vary-by-developer="true">
<vary-by-header>Accept</vary-by-header>
<vary-by-header>Accept-Encoding</vary-by-header>
</cache-lookup>
<!-- If not cached, cache the response -->
<cache-store duration="3600" />
</outbound>
On-Error Policies
Custom Error Response
<on-error>
<!-- Log the error -->
<log verbosity="error">
@(context.LastError.Message)
</log>
<!-- Return custom error -->
<return-response>
<set-status code="500" reason="Internal Server Error" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>{
"error": "An unexpected error occurred",
"requestId": "@(context.Request.Id)"
}</set-body>
</return-response>
</on-error>
Retry and Fallback
<on-error>
<choose>
<when condition="@(context.Response.StatusCode == 503)">
<!-- Try fallback backend -->
<set-backend-service base-url="https://fallback-api.example.com" />
<retry count="3" interval="5">
<forward-request />
</retry>
</when>
<otherwise>
<return-response>
<set-status code="500" />
<set-body>Service temporarily unavailable</set-body>
</return-response>
</otherwise>
</choose>
</on-error>
Policy Expressions
Access Context Variables
<!-- Request URL -->
<set-variable name="path" value="@(context.Request.Url.Path)" />
<!-- Request Method -->
<set-variable name="method" value="@(context.Request.Method.ToString())" />
<!-- Headers -->
<set-variable name="authToken" value="@(context.Request.Headers.GetValueOrDefault("Authorization", ""))" />
<!-- Response Status Code -->
<set-variable name="statusCode" value="@(context.Response.StatusCode)" />
Complex Expressions
<!-- Extract user ID from JWT -->
<set-variable name="userId" value="@{
var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
if (string.IsNullOrEmpty(authHeader)) return null;
var token = authHeader.Split(' ')[1];
var parts = token.Split('.');
if (parts.Length != 3) return null;
var payload = System.Text.Encoding.UTF8.GetString(
System.Convert.FromBase64String(parts[1] + "=="));
var json = System.Text.Json.JsonDocument.Parse(payload);
return json.RootElement.GetProperty("sub").GetString();
}" />
Condition Examples
<inbound>
<!-- Check if user is admin -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<bool>("isAdmin"))">
<!-- Admin can access all -->
</when>
<otherwise>
<!-- Non-admin: check resource ownership -->
<set-variable name="canAccess" value="@{
var userId = context.Request.Headers.GetValueOrDefault("X-User-Id", "");
var resourceOwnerId = context.Request.Url.Query.GetValueOrDefault("ownerId", "");
return userId == resourceOwnerId;
}" />
</otherwise>
</choose>
</inbound>
Advanced Patterns
Rate Limiting with Custom Key
<inbound>
<set-variable name="rateKey" value="@{
var userId = context.Request.Headers.GetValueOrDefault("X-User-Id", "");
var apiName = context.Api.Name;
return $"{apiName}:{userId}";
}" />
<rate-limit-by-key key="@(context.Variables["rateKey"])" calls="100" renewal-period="60" />
</inbound>
Validate Request Body
<inbound>
<validate-jwt header-name="Authorization">
<issuer-signing-keys>
<key>base64encodedkey</key>
</issuer-signing-keys>
<audiences>
<audience>api://my-api</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/tenant-id/</issuer>
</issuers>
</validate-jwt>
</inbound>
Transform Request to Backend
<inbound>
<!-- Convert XML to JSON -->
<xml-to-json policy="transform" apply="always" consider-accept-header="false" />
<!-- Set backend URL with path -->
<set-backend-service base-url="https://backend.example.com/api/v2" />
</inbound>
Best Practices
| Practice | Description |
|---|---|
| Use base policy | Include <base /> to inherit parent policies |
| Organize with comments | Add XML comments for clarity |
| Test in test console | Use APIM test console to validate policies |
| Version policies | Track policy changes in source control |
| Use named policies | Create reusable policy fragments |
| Log errors | Always include error logging |
Debugging
<!-- Trace policy execution -->
<trace>
@(context.Variables["variableName"])
</trace>
<!-- Log to APIM logs -->
<log level="information">
Processing request: @(context.Request.Id)
</log>
Related Topics
- JWT Validation — Authentication policies
- Rate Limiting — Throttling policies
- Caching — Response caching policies
Azure Integration Hub - Intermediate Level