DEV Community

Manohari Jayachandran
Manohari Jayachandran

Posted on

Azure API Management Part 1: Products, Policies, and Securing Your APIs

Azure API Management was one of the core pieces of the integration platform used at Blue Yonder to connect Salesforce and ServiceNow, alongside Logic Apps, Function Apps, and Service Bus, covered in earlier posts on this blog. APIM is also genuinely confusing the first time it needs to be configured rather than just read about. This is Part 1 of a two-part refresher - concepts, policies, and security. Part 2 will be a complete, real example: building an actual CRUD API and putting it behind APIM end to end.

What Azure API Management Actually Is

The core idea behind APIM is that it sits in front of one or more backend APIs, and callers talk to APIM, never directly to the backend. APIM checks identity, applies rate limits, can transform the request or response, and only then forwards the call to the real API.

APIM

Think of a hotel front desk. Guests don't walk directly into any room they want - they check in at the desk first, which verifies who they are, hands out a key card scoped to specific rooms, and only then lets them proceed. The rooms themselves, the backend APIs, never have to handle check-in logic directly - that's the desk's job, done once, consistently, for every guest.

Without APIM, every API has to implement its own auth check, its own rate limiting, and its own logging, and do it consistently across every single service. With APIM, a caller reaches APIM first, which handles auth, rate limiting, logging, and transformation once, centrally, for every API behind it, without duplicating that logic in every backend service.

The Core Hierarchy: Product, API, Operation

A Product is a bundle of one or more APIs presented together to consumers, with subscription and usage rules attached at the product level. An API is a specific backend service exposed through APIM. An Operation is a single endpoint within that API - one specific HTTP verb and route, like GET /orders/{id}.

A caller subscribes to the Product, which grants access to every API inside it. A single subscription key can call multiple different APIs, as long as both live inside the same product - an Orders API and an Inventory API bundled into one "Partner Integration APIs" product, for instance, both become reachable with the same subscription key.

This hierarchy matters because policies can be written at the Product level, the API level, or the individual Operation level, and they run in an inherited order - product-level policies apply first, then API-level, then operation-level, unless explicitly overridden. A rate limit set at the product level applies to every API inside it by default; a stricter rate limit set on one specific operation overrides that default for just that operation.

APIM

Publishing and Versioning

Versioning solves the problem of shipping a breaking change to an API without breaking every existing caller still using the old version. APIM supports several versioning schemes: path-based, where the version is visible directly in the URL as /v1/orders versus /v2/orders; query string-based, where the same URL accepts a version parameter like ?api-version=2; and header-based, where the URL stays identical and the version travels in a custom header instead.

A practical publishing flow looks like this: a new API version is created alongside the existing one, and both are live simultaneously. Existing callers keep hitting the original version exactly as before, with nothing breaking for them. New callers, or callers ready to migrate, opt into the new version explicitly. Once every caller has migrated off the old version, confirmed through APIM's analytics, the old version can be safely retired.

Path-based versioning is the most common choice specifically because it's the most discoverable - a caller can see directly in the URL which version they're hitting, which matters enormously when debugging an integration issue with an external partner months after the original setup.

Policies: XML That Runs in the Request and Response Pipeline

A policy is a block of XML that executes at specific points in a request's lifecycle - before it reaches the backend, after the backend responds, or if something fails along the way. There are four policy sections, run in order.

The <inbound> section runs before the request reaches the backend - authentication checks, rate limiting, request transformation, and header manipulation all happen here. The <backend> section is the actual call to the real backend API, rarely customized beyond the default forward-the-request behavior, though it can be modified for advanced routing scenarios. The <outbound> section runs on the response, before it reaches the caller - response transformation, header stripping, and adding CORS headers all happen here. The <on-error> section runs if anything in the sections above throws an error, and is commonly used for consistent error response formatting across every API behind APIM.

<policies>
  <inbound>
    <base />
    <!-- Check for a required custom header -->
    <check-header name="X-Client-Id"
        failed-check-httpcode="400"
        failed-check-error-message="X-Client-Id header is required"
        ignore-case="true" />

    <!-- Rewrite the backend URL -->
    <set-backend-service base-url="https://internal-api.company.com" />
  </inbound>

  <backend>
    <base />
  </backend>

  <outbound>
    <base />
    <!-- Strip an internal header before it reaches the caller -->
    <set-header name="X-Internal-Server-Id" exists-action="delete" />
  </outbound>

  <on-error>
    <base />
    <set-header name="X-Error-Source" exists-action="override">
      <value>APIM</value>
    </set-header>
  </on-error>
</policies>
Enter fullscreen mode Exit fullscreen mode

The <base /> element matters - it means "also run whatever policy is inherited from the level above, whether that's Product or API." Omitting it means this policy replaces the inherited one entirely rather than adding to it, which is a common source of confusion when a policy that worked at the Product level appears to stop applying once an Operation-level policy is added without including <base />.

Subscription Keys: The Simplest Layer of Access Control

A subscription key proves that the caller has a valid key - nothing more. It does not identify who is calling, only that the caller possesses a key associated with a specific Product subscription. A subscription key is sent as a header or query parameter on every request, and APIM validates that the key exists and is active for the Product this API belongs to before forwarding the request to the backend at all.

Think of a building's keycard system. Having a valid keycard proves you're allowed into the building - it does not prove you're a specific named employee versus a visitor who was issued a temporary card. Subscription keys work the same way: they gate access, but they aren't a real identity check.

Subscription keys alone are genuinely sufficient for internal tooling, low-risk read-only endpoints, or scenarios where the actual sensitive authorization happens downstream anyway. They're rarely sufficient on their own for anything handling real user data or financial operations.

OAuth Validation at the Gateway

Validating a JWT access token directly at the APIM gateway - the same OAuth mechanism covered in an earlier post on this blog about Azure AD token flows - rejects invalid or expired tokens before the request ever reaches the backend API.

<inbound>
  <base />
  <validate-jwt header-name="Authorization"
      failed-validation-httpcode="401"
      failed-validation-error-message="Unauthorized - invalid or missing token">
    <openid-config url="https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration" />
    <audiences>
      <audience>api://your-api-client-id</audience>
    </audiences>
    <required-claims>
      <claim name="roles" match="any">
        <value>Orders.Read</value>
      </claim>
    </required-claims>
  </validate-jwt>
</inbound>
Enter fullscreen mode Exit fullscreen mode

This single policy block validates the token's signature, checks it hasn't expired, confirms it was issued for the right audience, and confirms the caller has a specific required role claim, all before the backend API's own code runs at all.

Validating at the gateway matters, not just in the backend, because an invalid or expired token gets rejected immediately at APIM - the backend never spends compute processing a request that was going to fail authorization anyway. It also means every API behind APIM gets consistent token validation logic without each one implementing it separately, the exact same architectural benefit as centralizing rate limiting or logging.

Real production APIs often require both a subscription key and a valid OAuth token together - the subscription key controls which Product or plan the caller is on, while the OAuth token establishes their actual identity and permissions. Neither replaces the other; they answer different questions.

CORS: Why Browsers Block Requests That Postman Allows Fine

The actual confusion, named directly: a request works perfectly in Postman, curl, or the backend's own server-to-server calls, but fails specifically when called from JavaScript running in a browser, with an error mentioning "CORS policy" and no obvious HTTP status code to debug. This happens because CORS is not a backend rule at all - it's a browser-enforced security restriction, and the browser is the thing blocking the request, not the API.

Browsers enforce a same-origin policy by default - JavaScript running on one origin, a combination of scheme, domain, and port like https://techstackblog.com, cannot make requests to a different origin like https://api.techstackblog.com unless that other origin explicitly says it's allowed to. CORS, Cross-Origin Resource Sharing, is the mechanism a server uses to explicitly grant that permission via response headers.

Think of a guest list at a private event. The browser is the venue's security guard - by default, it doesn't let anyone in unless the event's host, the API, has explicitly published a guest list saying which other venues, or origins, are allowed to send their guests over. Postman and curl aren't guests arriving through the browser's front door at all - they're not subject to the guest list, which is exactly why the same request that fails from a browser works completely fine from those tools.

This is why CORS only affects browser-based JavaScript specifically. Postman, curl, or a server-to-server call trigger no CORS check at all, since CORS is a browser enforcement mechanism, not a server-side one. Browser JavaScript calling a different origin gets checked by the browser for CORS headers before letting the response reach the calling JavaScript code - even if the API actually processed and returned a valid response, the browser hides it from the page's JavaScript if the CORS headers are missing.

CORS

A second common source of confusion is the preflight request. For certain requests, anything beyond simple GET or POST with standard headers, the browser sends an OPTIONS request first, before the real request, asking "am I allowed to do this." This preflight request happens automatically and invisibly, before the actual GET, POST, or PUT the developer wrote code for. The server's response to that preflight needs to include headers like Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. Only if the preflight response includes the right headers does the browser then send the real request that the developer's code actually triggered.

APIM handles this through a dedicated CORS policy rather than requiring individual response headers to be hand-written:

<inbound>
  <base />
  <cors allow-credentials="true">
    <allowed-origins>
      <origin>https://techstackblog.com</origin>
    </allowed-origins>
    <allowed-methods>
      <method>GET</method>
      <method>POST</method>
      <method>PUT</method>
    </allowed-methods>
    <allowed-headers>
      <header>Content-Type</header>
      <header>Authorization</header>
    </allowed-headers>
  </cors>
</inbound>
Enter fullscreen mode Exit fullscreen mode

APIM's built-in CORS policy automatically handles the OPTIONS preflight request and adds the correct headers to the real response - both halves of the CORS handshake in one policy block, rather than manually writing separate logic for the preflight and the actual response.

The specific mistake that causes the most confusion is setting Access-Control-Allow-Origin to a wildcard, allowing any origin, while also trying to send credentials like cookies or authorization headers from the browser. Browsers explicitly forbid combining a wildcard origin with credentialed requests, since that combination would defeat the entire purpose of the restriction. If credentials need to be sent, the allowed origin must be a specific, named origin, not a wildcard.

Rate Limiting: Protecting the Backend From Being Overwhelmed

<!-- Product-level rate limit - applies to every
     API inside this product by default -->
<inbound>
  <base />
  <rate-limit-by-key calls="100"
      renewal-period="60"
      counter-key="@(context.Subscription.Id)" />
</inbound>
Enter fullscreen mode Exit fullscreen mode

This example allows 100 calls per 60 seconds, tracked per subscription. Exceeding this returns a 429 Too Many Requests response automatically, without the backend ever being called.

<!-- A stricter limit on one specific operation,
     overriding the product-level default -->
<inbound>
  <base />
  <rate-limit-by-key calls="10"
      renewal-period="60"
      counter-key="@(context.Subscription.Id)" />
</inbound>
Enter fullscreen mode Exit fullscreen mode

This specific operation, perhaps an expensive report-generation endpoint, gets a much stricter limit than the product default, because it's genuinely more expensive to serve.

The real point of rate limiting is that it happens in the inbound policy, before the backend call - that's what makes it actually protective rather than just informational. A rate limit checked after the backend already processed the request has already spent the compute it was meant to prevent.

A product-level limit is the sensible default for ensuring no single subscriber can overwhelm anything behind that product. An operation-level override makes sense when one specific endpoint is disproportionately expensive to serve, such as a search or report-generation endpoint, and needs a tighter limit than the rest of the product.

Key Lessons

APIM sits in front of backend APIs - callers talk to APIM, never directly to the real service, which centralizes auth, rate limiting, and transformation instead of duplicating that logic in every backend.

The Product, API, Operation hierarchy determines where a policy lives, and policies inherit downward unless explicitly overridden - this is the most common source of "why isn't my policy applying" confusion.

Path-based versioning is the most discoverable scheme, letting existing callers on the old version keep working unaffected while new callers opt into the new version explicitly.

Policies run in four sections - inbound, backend, outbound, on-error - and <base /> matters, since omitting it replaces the inherited policy instead of adding to it.

A subscription key proves possession of a key, not identity - OAuth validation at the gateway is what establishes a real, checkable identity before the backend ever runs.

CORS is a browser-enforced restriction, not a server-side one - Postman and server-to-server calls never trigger it, which is exactly why "it works in Postman but not the browser" is the classic CORS symptom.

Rate limiting belongs in the inbound policy specifically because that's what actually protects the backend - checking after the fact only observes the problem rather than preventing it.

What's Next

Part 2 puts all of this into practice - a complete, real example building a CRUD API and putting it behind APIM end to end, with actual products, policies, versioning, and security configured from scratch.

Summary

Azure API Management is the front desk in front of one or more real backend APIs, centralizing authentication, rate limiting, and request and response transformation so that logic doesn't need to be duplicated in every individual service. The Product, API, and Operation hierarchy determines where policies live and how they inherit. Subscription keys and OAuth validation answer different questions - possession of a key versus actual identity - and real production APIs frequently need both. Rate limiting, applied in the inbound policy before the backend is ever called, is what makes it genuinely protective. Understanding these pieces individually is what makes APIM's XML policy language make sense as a coherent system rather than a collection of unrelated snippets to copy and paste.


Originally published at TechStack Blog: https://www.techstackblog.com/post.html?slug=azure-apim-explained-part1

Related reading on TechStack Blog:
Azure AD and OAuth Token Flows: https://www.techstackblog.com/post.html?slug=azure-ad-oauth-token-flows-explained
Azure API Management Deep Dive: https://www.techstackblog.com/post.html?slug=azure-api-management-deep-dive

More from TechStack Blog: Azure: https://www.techstackblog.com/category.html?cat=azure

Top comments (0)