🛸 Hitchhiker's Guide — Phase 09: RESTful APIs, JSON & Secure AuthN/AuthZ

Read this if: you can build a REST controller but "design our API edge" means something vaguer than it should. The gateway, the status codes, the idempotency, the rate limiter — they're a small set of sharp rules, and this is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP, then build the gateway.


0. The 30-second mental model

Every request to a managed API crosses a gateway first, and the gateway makes the same four decisions in the same order: does this route exist (404) → who is this and may they (401/403) → are they over budget (429) → call the backend (200). One sentence to tattoo: the gateway is a pipeline, and the order is the security policy — route before auth (nothing to authorize otherwise), auth before rate-limit (don't let probing cost rate budget), backend last (it's the expensive thing you're shielding).

1. The methods, one breath

GET    safe, idempotent, cacheable   read
HEAD   safe, idempotent              read headers
PUT    idempotent (NOT safe)         replace wholesale → retry-safe
DELETE idempotent (NOT safe)         remove            → retry-safe
POST   neither                       create/process    → NOT retry-safe
PATCH  not guaranteed idempotent     partial update    → retry only if you made it so

Idempotent = doing it twice == doing it once == safe to retry on a timeout. That single property is why PUT-to-a-known-id and POST-with-an-idempotency-key are how principals design writes — so a client can retry through the network's "did it happen?" fog.

2. The status codes to tattoo on your arm

CodeMeansClient should
200/201/204OK / Created / No Contentproceed
400malformed requestfix the request, don't retry as-is
401un-authenticated (no/bad/expired token)re-authenticate
403authenticated, not authorized (wrong scope)get more scope (re-auth won't help)
404no such route/resourcecheck the URL
409conflict (optimistic concurrency)re-read, re-apply
415/406wrong Content-Type / Acceptfix media type
429over rate/quotaback off Retry-After seconds
5xxserver failed (transient)retry with exponential backoff (P14)

The one rule under all of it: 4xx is your fault (don't retry unchanged), 5xx is our fault (retry with backoff), 429 is "slow down" (back off, then retry).

3. 401 vs 403 vs 429 — the thing they actually test

401  "I don't know you."            → refresh your token
403  "I know you. The answer's no." → ask for more scope
429  "I know you, you're allowed,   → wait Retry-After, retry
      but not this fast."

Get 401/403 backwards and you cause an incident: return 401 on a forbidden call and a smart client refreshes its token and retries the forbidden call forever (refresh storm

  • hot loop). Return 403 on an expired token and the client gives up instead of refreshing.

4. The APIM nouns (use them and you sound like you've shipped one)

Product  = bundle of APIs + a subscription (key) + product policies
  API    = operations sharing a base path + backend
  Operation = one METHOD + URL template (GET /users/{id})
  Policy = inbound → backend → outbound → on-error

Policies compose top-down (global → product → API → operation), like nested middleware. The inbound section is where 90% of the action is: validate-jwt, rate-limit-by-key, transform, route. An inbound policy can short-circuit the whole pipeline (a failed validate-jwt returns 401 and the backend is never called).

5. The numbers you'll actually use

ThingNumber / rule
Idempotent methodsGET HEAD PUT DELETE (+ OPTIONS/TRACE); not POST, not PATCH
Retry-After on 429(1 − tokens) / refill_per_sec, rounded up, min 1 s (RFC 9110 §10.2.3)
Token buckettokens = min(capacity, tokens + elapsed·refill); spend 1 per call
capacity vs refillcapacity = burst you tolerate; refill = sustained rate you guarantee
Rate-limit vs quotarate-limit = calls/second (burst); quota = calls/day (fairness/billing)
Clock skew (JWT exp)Entra default leeway ~±300 s (P03)
scp vs rolesscp = delegated (user present, space-delimited); roles = app-only (daemon)
Pagination defaultcursor/keyset, not offset (offset drifts + slows at depth)

6. az / policy one-liners

# Create an APIM instance (Consumption tier = serverless, pay-per-call).
az apim create -n my-apim -g rg --publisher-email me@x.com --publisher-name "Me" --sku-name Consumption

# Import an API from an OpenAPI spec.
az apim api import -g rg --service-name my-apim --api-id users \
  --path users --specification-format OpenApi --specification-url https://.../openapi.json

# Show a subscription key (the per-consumer identity/meter).
az apim subscription list -g rg --service-name my-apim -o table
<!-- The two policies that are 90% of the edge: authenticate + throttle. -->
<inbound>
  <validate-jwt header-name="Authorization" failed-validation-httpcode="401">
    <openid-config url="https://login.microsoftonline.com/{tid}/v2.0/.well-known/openid-configuration" />
    <audiences><audience>api://my-api</audience></audiences>          <!-- aud check: anti confused-deputy -->
    <required-claims><claim name="scp" match="all"><value>users.read</value></claim></required-claims>
  </validate-jwt>                                                      <!-- missing scope → 403 -->
  <rate-limit-by-key calls="10" renewal-period="1"
                     counter-key="@(context.Subscription.Key)" />     <!-- 10/s per consumer; over → 429 -->
</inbound>

7. War story shapes you'll relive

  • "Clients are hammering Entra with token refreshes." → someone returned 401 for an authorization failure. The client thinks the token's bad, refreshes, retries the same forbidden call, loops. Fix: it's a 403. The code is the contract.
  • "We double-charged customers during the outage." → retry-on-5xx logic replayed a non-idempotent POST /charge. Fix: idempotency keys, so a replay is a no-op.
  • "/users/me returns the wrong user." → route matching picked /users/{id} with id="me" over the literal /users/me. Fix: most-specific-wins (literal beats wildcard).
  • "The throttle isn't working / is too aggressive." → confusing rate-limit (burst) with quota (monthly), or sizing capacity (burst) wrong. Draw the bucket; do the arithmetic.
  • "Service A accepts service B's token."validate-jwt checked the signature but not aud. Confused deputy. Always check aud.
  • "Our 429s aren't helping." → no Retry-After, so clients retry too fast and make it worse. Emit the precise back-off.

8. Vocabulary that signals you've held the pager

  • Idempotent — twice == once; the property that makes a write retry-safe.
  • Confused deputy — a service that honors a token minted for someone else (skipped aud).
  • Fail closed — the default decision is deny; you only allow what you can prove.
  • Token bucket — burst capacity + sustained refill; the canonical rate limiter.
  • Noisy neighbor — one tenant's flood starving others; rate-limit-by-key is the bulkhead.
  • Policy pipeline — APIM's inbound → backend → outbound → on-error; order is the policy.
  • scp vs roles — delegated (a user is present) vs app-only (a daemon).
  • Cursor pagination — opaque "last key I saw"; stable under writes, unlike offset.
  • Content negotiationAccept/Content-Type dance; mismatch is 406/415.

9. Beginner mistakes that mark you in interviews

  1. Saying "REST" but designing RPC (POST /api/doStuff) — no resources, no uniform methods.
  2. Confusing 401 and 403 (the single most common edge bug, and it causes real incidents).
  3. Returning 200 with an error in the body — breaks every layer that reads the status.
  4. Retrying a POST on a timeout without an idempotency key — duplicate writes.
  5. 429 without Retry-After — clients retry too fast and amplify the overload.
  6. Checking the JWT signature but not aud — the confused-deputy breach.
  7. Rate-limiting before authenticating — letting unauth probes drain rate budget.
  8. Route matching that lets /users/{id} shadow the literal /users/me.
  9. Offset pagination on a write-heavy collection — skipped/duplicated rows at the seams.

10. How this phase pays off later

  • The gateway pipeline is where reliability (P14: 429/Retry-After, circuit breaking), identity (P03: the JWT this phase validates), and observability (P13: every 4xx/5xx is a KQL signal) all meet — it's the edge of the whole platform.
  • Idempotency recurs everywhere: ARM PUT (P01), Service Bus dedup (P10), Durable replay (P11). The "twice == once" instinct you build here is a track-wide muscle.
  • scp/roles authorization is Phase 03's token, consumed. This is where minting and validating a token turns into gating real operations with it.
  • The token-bucket math is the same 429/Retry-After arithmetic you'll compose into a retry budget and a circuit breaker in P14.

Now read the WARMUP slowly, then build the gateway. You'll reach for its pipeline order and its 401/403 rule in every API design review for the rest of your career.