Intermediate Examples
Examples 29-56 cover versioning strategy (URI path, header, query parameter) and evolution
discipline (additive changes, the tolerant reader, consumer contract tests, Deprecation/Sunset
headers), idempotent writes with Idempotency-Key (recording, replaying, and rejecting a body
mismatch), rate limiting (429/Retry-After, the structured RateLimit/RateLimit-Policy
headers, and a quota counter draining to zero), caching and optimistic concurrency (ETag/
If-None-Match/304, Cache-Control, If-Match), authentication and authorization (bearer tokens,
API keys, OAuth scopes, and declaring both in an OpenAPI security scheme), partial responses
(?fields= and JSON:API sparse fieldsets), batch and bulk operations, and webhooks (subscribing,
then signing the outbound payload with HMAC). Every example is a complete, self-contained,
originally-authored Python 3 script using only the standard library. Run each with
python3 example.py; every printed line below was captured from an actual run of the file shown.
Example 29: Versioning via the URI Path
ex-29 · exercises co-13
Routing /v1/... and /v2/... to different handlers is the most visible versioning strategy --
Google's AIP-185 recommends "v1", not "v1.0", with the version baked directly into every request's
own path.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A[Client Request] --> B{Versioning strategy}
B -->|Example 29: URI path| C["/v1/articles vs /v2/articles"]
B -->|Example 30: header| D["Stripe-Version: 2026-01-01"]
B -->|Example 31: query param| E["?api-version=2026-01-01"]
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#000000,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#000000,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
class A blue
class B orange
class C teal
class D purple
class E brown
learning/code/ex-29-version-uri-path/example.py
# pyright: strict
"""Example 29: Versioning via the URI Path. (co-13)
Routing `/v1/...` and `/v2/...` to different handlers is the most visible
versioning strategy -- Google's AIP-185 recommends "v1", not "v1.0", and the
version lives directly in every request's own path.
"""
from typing import Callable # => a handler is just a function, typed for clarity
ROUTES: dict[str, Callable[[], dict[str, object]]] = { # => co-13: path prefix -> its own handler
"/v1/articles/1": lambda: {"id": 1, "title": "Hello"}, # => v1 shape: two fields
"/v2/articles/1": lambda: {"id": 1, "title": "Hello", "author": "Ada"}, # => v2 adds a field
} # => end of ROUTES
# => ROUTES has 2 keys, each mapped to a zero-arg callable returning that version's own shape
def dispatch(path: str) -> dict[str, object]: # => co-13: routes purely by the path's own prefix
handler = ROUTES[path] # => co-13: the version is baked into the URI itself
return handler() # => calls whichever version's handler matched
v1_result = dispatch("/v1/articles/1") # => request 1: explicitly asks for v1
print(f"v1: {v1_result}") # => Output: {'id': 1, 'title': 'Hello'}
v2_result = dispatch("/v2/articles/1") # => request 2: explicitly asks for v2
# => v2_result has one MORE key ("author") than v1_result -- a different URI, a different shape
print(f"v2: {v2_result}") # => Output: {'id': 1, 'title': 'Hello', 'author': 'Ada'}Run: python3 example.py
Output:
v1: {'id': 1, 'title': 'Hello'}
v2: {'id': 1, 'title': 'Hello', 'author': 'Ada'}Key takeaway: The version is part of the URI itself -- two callers hitting /v1/articles/1 and
/v2/articles/1 are, in effect, hitting two distinct resources that happen to represent the same
underlying data.
Why it matters: URI-path versioning is the most discoverable strategy -- a caller (or a browser,
or a cache) can see the version just by reading the URL, and old and new versions can be routed,
cached, and rate-limited independently since they are, mechanically, different paths. This discoverability also pays off operationally: a load balancer or reverse proxy can route /v1/ and /v2/ to entirely separate backend deployments with a simple path rule, no header inspection required.
Example 30: Versioning via a Request Header
ex-30 · exercises co-13
Stripe pins a version per API key via Stripe-Version, rather than baking it into the URI -- the
SAME path resolves differently depending purely on a header's value, keeping every version's URI
identical.
learning/code/ex-30-version-header/example.py
# pyright: strict
"""Example 30: Versioning via a Request Header. (co-13)
Stripe pins a version per API key via `Stripe-Version`, rather than baking
it into the URI -- the SAME path resolves differently depending purely on a
header's value, keeping every version's URI identical.
"""
from typing import Callable # => a handler is just a function, typed for clarity
HANDLERS: dict[str, Callable[[], dict[str, object]]] = { # => co-13: header value -> its own handler
"2025-01-01": lambda: {"id": 1, "title": "Hello"}, # => the OLDER API-Version shape
"2026-01-01": lambda: {"id": 1, "title": "Hello", "author": "Ada"}, # => the NEWER shape
} # => end of HANDLERS
def dispatch(path: str, api_version: str) -> dict[str, object]: # => co-13: routes by header, not path
handler = HANDLERS[api_version] # => co-13: the SAME path, resolved by a header value instead
return handler() # => calls whichever version's handler the header selected
old = dispatch("/articles/1", api_version="2025-01-01") # => request 1: the older header value
print(f"2025-01-01: {old}") # => Output: {'id': 1, 'title': 'Hello'}
new = dispatch("/articles/1", api_version="2026-01-01") # => request 2: SAME path, newer header
# => both calls used the identical path "/articles/1" -- only the header value changed the shape
print(f"2026-01-01: {new}") # => Output: {'id': 1, 'title': 'Hello', 'author': 'Ada'}Run: python3 example.py
Output:
2025-01-01: {'id': 1, 'title': 'Hello'}
2026-01-01: {'id': 1, 'title': 'Hello', 'author': 'Ada'}Key takeaway: Unlike Example 29, the path never changes -- dispatch resolves purely from the
api_version header, so a bookmark or a cache key built only from the URI can no longer
distinguish the two versions.
Why it matters: Header-based versioning keeps a resource's URI stable across every version, which matters when the URI itself is treated as a permanent identifier (bookmarked, embedded in another system) -- the cost is that the version becomes invisible to anything that only looks at the URL, such as a browser address bar or a plain access log.
Example 31: Versioning via a Query Parameter
ex-31 · exercises co-13
Azure's guideline uses ?api-version=YYYY-MM-DD, and treats a MISSING parameter as an error (400 MissingApiVersionParameter) rather than a silent default -- forcing every caller to be explicit
about its version.
learning/code/ex-31-version-query/example.py
# pyright: strict
"""Example 31: Versioning via a Query Parameter. (co-13)
Azure's guideline uses `?api-version=YYYY-MM-DD`, and treats a MISSING
parameter as an error (`400 MissingApiVersionParameter`) rather than a
silent default -- forcing every caller to be explicit about its version.
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-13: status plus a small JSON-shaped body
class Response:
status: int # => the HTTP status code
body: dict[str, object] # => either the resolved data or an error description
def get_article(api_version: str | None) -> Response: # => GET /articles/1?api-version=
if api_version is None: # => co-13: Azure's own rule -- missing means reject, not default
return Response(400, {"error": "MissingApiVersionParameter"}) # => 400, explicit rejection
return Response(200, {"id": 1, "title": "Hello", "api_version": api_version}) # => echoes the version
missing = get_article(api_version=None) # => request 1: no ?api-version= at all
# => missing.status is 400 -- Azure's rule rejects silence, unlike most other version strategies
print(f"missing: status={missing.status}, body={missing.body}") # => Output: 400, MissingApiVersionParameter
present = get_article(api_version="2026-01-01") # => request 2: explicit version given
print(f"present: status={present.status}, body={present.body}") # => Output: 200, echoes the versionRun: python3 example.py
Output:
missing: status=400, body={'error': 'MissingApiVersionParameter'}
present: status=200, body={'id': 1, 'title': 'Hello', 'api_version': '2026-01-01'}Key takeaway: Azure's own guideline rejects a missing ?api-version= rather than defaulting it
-- an explicit 400 forces every integration to state its version up front, instead of silently
riding whatever the "current default" happens to be.
Why it matters: A silent default version is a trap: it works fine until the default changes, then every caller that never specified a version breaks at once, with no warning. Rejecting the missing parameter converts that future breakage into an immediate, loud integration-time error. In practice, the loud error is far cheaper: it surfaces during integration testing, on a developer's machine, instead of during a production traffic spike when the default silently shifts under everyone at once.
Example 32: Adding an Optional Field Is Backward-Compatible
ex-32 · exercises co-14
Adding a NEW, optional field to a response never breaks an existing client that only reads the fields it already knew about -- this is the single most common, safest way an API evolves without a version bump.
learning/code/ex-32-additive-change-compatible/example.py
# pyright: strict
"""Example 32: Adding an Optional Field Is Backward-Compatible. (co-14)
Adding a NEW, optional field to a response never breaks an existing client
that only reads the fields it already knew about -- this is the single most
common, safest way an API evolves without a version bump.
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-14: the CURRENT shape a v1 client was written against
class ArticleV1:
id: int # => the article's own id
title: str # => the article's own title
def get_article_v1() -> ArticleV1: # => the ORIGINAL handler, unchanged
return ArticleV1(id=1, title="Hello") # => the shape a v1 client already expects
def get_article_evolved() -> dict[str, object]: # => co-14: the SAME endpoint, now with one MORE field
return {"id": 1, "title": "Hello", "author": "Ada"} # => co-14: additive -- nothing removed or renamed
def old_client_reads(response: dict[str, object]) -> ArticleV1: # => co-14: reads ONLY id/title, like before
return ArticleV1(id=response["id"], title=response["title"]) # type: ignore[arg-type] # => old fields only
original = get_article_v1() # => the shape before the change
print(f"before: {original}") # => Output: ArticleV1(id=1, title='Hello')
evolved_response = get_article_evolved() # => the SAME endpoint, after adding "author"
# => evolved_response has 3 keys now, but old_client_reads only ever looks at 2 of them
old_view = old_client_reads(evolved_response) # => co-14: an old client, unaware "author" now exists
print(f"old client still works: {old_view}") # => Output: ArticleV1(id=1, title='Hello') -- unaffectedRun: python3 example.py
Output:
before: ArticleV1(id=1, title='Hello')
old client still works: ArticleV1(id=1, title='Hello')Key takeaway: old_client_reads never looks at "author" -- so adding that field to the
server's response changes nothing from the old client's point of view; it produces the exact same
ArticleV1(id=1, title='Hello') both before and after the change.
Why it matters: Distinguishing "additive" from "breaking" changes is the core skill behind avoiding unnecessary version bumps (Examples 29-31) -- most real API evolution is additive, and treating every change as a version bump trains callers to ignore version numbers entirely. Getting this distinction wrong in either direction has a real cost: bumping the version too often trains callers to ignore it, while missing a genuine breaking change trains them to trust an unsafe upgrade.
Example 33: The Tolerant Reader -- a Client That Ignores Unknown Fields
ex-33 · exercises co-14, co-02
Consumer-driven design (co-02) means a client reads defensively: it extracts ONLY the fields it
needs via .get(), so a server adding an unrelated field never breaks it -- the client tolerates
what it does not recognize.
learning/code/ex-33-tolerant-reader/example.py
# pyright: strict
"""Example 33: The Tolerant Reader -- a Client That Ignores Unknown Fields. (co-14, co-02)
Consumer-driven design (co-02) means a client reads defensively: it extracts
ONLY the fields it needs via `.get()`, so a server adding an unrelated field
never breaks it -- the client tolerates what it does not recognize.
"""
from dataclasses import dataclass # => a small typed record for what THIS client actually needs
@dataclass # => co-02: only the fields this ONE client cares about, nothing more
class ClientView:
id: int # => the only field this client reads
title: str # => the only OTHER field this client reads
def tolerant_read(response: dict[str, object]) -> ClientView: # => co-14/co-02: reads defensively
return ClientView( # => builds the client's own narrow view of a possibly-larger response
id=response.get("id", 0), # type: ignore[arg-type] # => co-02: .get(), never response["id"]
title=response.get("title", ""), # type: ignore[arg-type] # => co-02: tolerates a missing key too
) # => end of the ClientView construction
v1_response: dict[str, object] = {"id": 1, "title": "Hello"} # => the ORIGINAL server shape
v2_response: dict[str, object] = {"id": 1, "title": "Hello", "author": "Ada", "views": 42}
# => co-14: the server later added TWO new fields the client never asked about
view_from_v1 = tolerant_read(v1_response) # => reads the original shape
print(f"from v1 response: {view_from_v1}") # => Output: ClientView(id=1, title='Hello')
view_from_v2 = tolerant_read(v2_response) # => co-02: reads the EXPANDED shape identically
# => view_from_v2 == view_from_v1 -- the two extra keys never even reach ClientView
print(f"from v2 response (unaffected): {view_from_v2}") # => Output: identical -- new fields ignoredRun: python3 example.py
Output:
from v1 response: ClientView(id=1, title='Hello')
from v2 response (unaffected): ClientView(id=1, title='Hello')Key takeaway: tolerant_read produces the exact same ClientView from both the original and
the expanded response -- .get() on fields the client actually needs is what makes the two extra
fields (author, views) irrelevant to it.
Why it matters: Example 32 showed the SERVER side of additive compatibility; this example shows
the CLIENT-side discipline that makes it actually safe in practice -- a client that indexes with
response["author"] unconditionally, or asserts on the exact set of keys present, forfeits the
compatibility the server tried to preserve. A client written this way survives a server team adding a new field next quarter with zero code changes on the client side -- exactly the compatibility additive changes are meant to preserve.
Example 34: A Consumer Contract Test Catches a Breaking Change
ex-34 · exercises co-14
A consumer contract test asserts the fields a real client depends on are still present. Removing a field the test asserts on makes the test fail LOUDLY, at build time -- exactly the point where a breaking change should be caught, not discovered by a client in production.
learning/code/ex-34-breaking-change-detect/example.py
# pyright: strict
"""Example 34: A Consumer Contract Test Catches a Breaking Change. (co-14)
A consumer contract test asserts the fields a real client depends on are
still present. Removing a field the test asserts on makes the test fail
LOUDLY, at build time -- exactly the point where a breaking change should be
caught, not discovered by a client in production.
"""
def get_article_v1() -> dict[str, object]: # => the ORIGINAL, contract-conforming response
return {"id": 1, "title": "Hello", "legacy_field": "still here"} # => the field the contract needs
def get_article_broken() -> dict[str, object]: # => co-14: a hypothetical FUTURE, breaking change
return {"id": 1, "title": "Hello"} # => "legacy_field" was REMOVED -- a real consumer still needs it
def consumer_contract_test(response: dict[str, object]) -> None: # => co-14: the test a CI pipeline runs
assert "id" in response, "contract violation: 'id' missing" # => still-required field 1
assert "title" in response, "contract violation: 'title' missing" # => still-required field 2
assert "legacy_field" in response, "contract violation: 'legacy_field' missing" # => the field at risk
consumer_contract_test(get_article_v1()) # => co-14: passes silently -- the contract still holds
print("v1 response: contract test passed") # => Output: contract test passed
try: # => co-14: run the SAME test against the hypothetical breaking change
consumer_contract_test(get_article_broken()) # => this call is expected to fail
print("broken response: contract test passed (UNEXPECTED)") # => would only print if the bug went uncaught
except AssertionError as exc: # => co-14: the breaking change is caught HERE, loudly
# => exc's message is "contract violation: 'legacy_field' missing" -- names the exact failure
print(f"broken response: contract test FAILED as expected: {exc}") # => Output: caught, with a clear reasonRun: python3 example.py
Output:
v1 response: contract test passed
broken response: contract test FAILED as expected: contract violation: 'legacy_field' missingKey takeaway: The SAME consumer_contract_test function runs against both responses -- it
passes against get_article_v1 and fails, with a specific and readable message, against
get_article_broken, precisely because "legacy_field" was removed.
Why it matters: A consumer contract test turns "did I just break a real client?" from a production incident into a build-time assertion -- it is the automated complement to the tolerant reader (Example 33): the server team runs a test that encodes what a real consumer actually reads, so a breaking change fails CI before it ever reaches that consumer.
Example 35: The Deprecation Header
ex-35 · exercises co-15
RFC 9745's Deprecation header notifies a caller that an endpoint (or the whole API version) is
deprecated, optionally paired with a Link header pointing at the replacement -- notification only,
the endpoint still works.
learning/code/ex-35-deprecation-header/example.py
# pyright: strict
"""Example 35: The Deprecation Header. (co-15)
RFC 9745's `Deprecation` header notifies a caller that an endpoint (or the
whole API version) is deprecated, optionally paired with a `Link` header
pointing at the replacement -- notification only, the endpoint still works.
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-15: status, headers (carrying the notice), and the still-working body
class Response: # => co-15: deprecation is signaled via headers, never via a different status
status: int # => the HTTP status code -- deprecation does NOT change this
headers: dict[str, str] # => carries the Deprecation + Link notice
body: dict[str, object] # => the endpoint still returns real data
def get_article_v1(article_id: int) -> Response: # => GET /v1/articles/{id} -- now deprecated
return Response( # => co-15: still succeeds, but carries a notice
status=200, # => the request STILL succeeds -- deprecation is a notice, not a rejection
headers={ # => the two headers RFC 9745 defines for this purpose
"Deprecation": "true", # => co-15: signals this endpoint is deprecated
"Link": '</v2/articles/1>; rel="successor-version"', # => co-15: points at the replacement
}, # => end of the headers dict
body={"id": article_id, "title": "Hello"}, # => the deprecated endpoint's own real data
) # => end of the Response construction
response = get_article_v1(1) # => call the deprecated endpoint
print(f"status={response.status}, body={response.body}") # => Output: 200, real data -- still works
print(f"Deprecation={response.headers['Deprecation']}") # => Output: Deprecation=true
print(f"Link={response.headers['Link']}") # => Output: points at the v2 successor
# => a client can automate migration by parsing the Link header's rel="successor-version"Run: python3 example.py
Output:
status=200, body={'id': 1, 'title': 'Hello'}
Deprecation=true
Link=</v2/articles/1>; rel="successor-version"Key takeaway: status stays 200 -- Deprecation and Link ride alongside a completely
normal, successful response; deprecation is communicated entirely through headers, never by
changing the status code or breaking the response.
Why it matters: A header-based deprecation notice lets automated tooling (a linter, a CI check,
a client-side warning log) surface "you are calling something deprecated" without the caller having
to read release notes -- and because the endpoint keeps working, the notice arrives with no forced
migration deadline attached (that is what Example 36's Sunset header adds).
Example 36: The Sunset Header
ex-36 · exercises co-15
RFC 8594's Sunset header names the DATE an endpoint will actually stop working -- distinct from
Deprecation (Example 35), which only says "deprecated," with no promised retirement date attached.
learning/code/ex-36-sunset-header/example.py
# pyright: strict
"""Example 36: The Sunset Header. (co-15)
RFC 8594's `Sunset` header names the DATE an endpoint will actually stop
working -- distinct from `Deprecation` (Example 35), which only says
"deprecated," with no promised retirement date attached.
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-15: status, headers (carrying the retirement date), and the still-working body
class Response:
status: int # => the HTTP status code -- a future sunset date does NOT change this yet
headers: dict[str, str] # => carries the Sunset notice
body: dict[str, object] # => the endpoint still returns real data, until the sunset date
def get_article_v1(article_id: int) -> Response: # => GET /v1/articles/{id} -- a scheduled retirement
return Response( # => co-15: still succeeds, but names the exact retirement date
status=200, # => still succeeds -- Sunset alone does not reject a request
headers={"Sunset": "Wed, 01 Jul 2026 00:00:00 GMT"}, # => co-15: RFC 8594's own date format
body={"id": article_id, "title": "Hello"}, # => real data, right up until the sunset date
) # => end of the Response construction
def client_warning(response: Response) -> str | None: # => co-15: a client that surfaces the notice
sunset_date = response.headers.get("Sunset") # => reads the header if present
if sunset_date is None: # => nothing to warn about
return None # => no Sunset header means no scheduled retirement
return f"warning: this endpoint retires on {sunset_date}" # => a human-facing warning string
response = get_article_v1(1) # => call the endpoint with a scheduled sunset
print(f"status={response.status}, body={response.body}") # => Output: 200, real data -- still works
warning = client_warning(response) # => co-15: extracts the human-facing warning
# => warning is "warning: this endpoint retires on Wed, 01 Jul 2026 00:00:00 GMT" (type: str)
print(f"client sees: {warning}") # => Output: warning naming the exact retirement dateRun: python3 example.py
Output:
status=200, body={'id': 1, 'title': 'Hello'}
client sees: warning: this endpoint retires on Wed, 01 Jul 2026 00:00:00 GMTKey takeaway: Sunset carries an exact, parseable retirement DATE, not just a boolean
"deprecated" flag -- client_warning can surface that specific date to a human, and the same date
could just as easily drive an automated alert when it gets close.
Why it matters: Deprecation and Sunset answer two different questions a caller needs
answered separately -- "should I plan a migration?" (Example 35) and "by when, exactly, must that
migration be done?" (this example) -- pairing them gives a caller both the warning and the deadline
in machine-readable form. Together they let an automated dashboard flag every caller still hitting a sunsetting endpoint, ranked by days remaining, instead of a human manually cross-referencing two separate pieces of information.
Example 37: Recording an Idempotency-Key on a Write
ex-37 · exercises co-18
An Idempotency-Key header (Stripe prior art -- the matching IETF draft is lapsed) lets a POST be
safely retried: the server records the key alongside the response it produced, the foundation
Examples 38-39 build on.
stateDiagram-v2
[*] --> NoKeyRecorded
NoKeyRecorded --> KeyRecorded: Example 37 - first write
KeyRecorded --> KeyRecorded: Example 38 - replay response
KeyRecorded --> Rejected422: Example 39 - mismatch 422
Rejected422 --> [*]
learning/code/ex-37-idempotency-key-write/example.py
# pyright: strict
"""Example 37: Recording an Idempotency-Key on a Write. (co-18)
An `Idempotency-Key` header (Stripe prior art -- the matching IETF draft is
lapsed) lets a POST be safely retried: the server records the key alongside
the response it produced, the foundation Examples 38-39 build on.
"""
from dataclasses import dataclass # => a small typed response record for this example
IDEMPOTENCY_STORE: dict[str, dict[str, object]] = {} # => co-18: key -> the response it produced
# => IDEMPOTENCY_STORE starts empty (type: dict[str, dict[str, object]])
@dataclass # => co-18: status plus the created resource's own body
class Response:
status: int # => the HTTP status code
body: dict[str, object] # => the created resource
def create_charge(idempotency_key: str, amount: int) -> Response: # => POST /charges
body: dict[str, object] = {"id": 1, "amount": amount, "status": "succeeded"} # => explicit dict[str, object]
IDEMPOTENCY_STORE[idempotency_key] = body # => co-18: records the key -> response pairing
return Response(status=201, body=body) # => a normal 201 Created response
response = create_charge(idempotency_key="key-abc-123", amount=5000) # => the FIRST call with this key
# => response.body is {'id': 1, 'amount': 5000, 'status': 'succeeded'}
print(f"status={response.status}, body={response.body}") # => Output: 201, the charge just created
print(f"recorded under key: {IDEMPOTENCY_STORE['key-abc-123']}") # => Output: the same body, storedRun: python3 example.py
Output:
status=201, body={'id': 1, 'amount': 5000, 'status': 'succeeded'}
recorded under key: {'id': 1, 'amount': 5000, 'status': 'succeeded'}Key takeaway: IDEMPOTENCY_STORE ends up holding the exact same body that was just returned to
the caller -- the server keeps its own record of "this key produced this response," which is the
data Example 38's replay logic depends on.
Why it matters: A POST that creates a resource is inherently non-idempotent (Example 4) --
retrying it after a timeout risks a duplicate. Idempotency-Key is the mechanism that lets a
client safely retry a POST anyway, by giving the server enough information to recognize "this is
the same logical request, not a new one."
Example 38: Replaying the Same Idempotency-Key
ex-38 · exercises co-18
Resending the identical Idempotency-Key returns the STORED response instead of re-executing the
write -- a counted side effect (here, a charge counter) proves the operation applied exactly once,
not twice.
learning/code/ex-38-idempotency-replay/example.py
# pyright: strict
"""Example 38: Replaying the Same Idempotency-Key. (co-18)
Resending the identical `Idempotency-Key` returns the STORED response
instead of re-executing the write -- a counted side effect (here, a charge
counter) proves the operation applied exactly once, not twice.
"""
from dataclasses import dataclass # => a small typed response record for this example
IDEMPOTENCY_STORE: dict[str, dict[str, object]] = {} # => co-18: key -> the response it produced
CHARGE_COUNT = [0] # => a mutable counter cell -- proves the side effect ran only once
@dataclass # => co-18: status plus the (possibly replayed) resource body
class Response:
status: int # => the HTTP status code
body: dict[str, object] # => the charge, whether freshly created or replayed
def create_charge(idempotency_key: str, amount: int) -> Response: # => POST /charges, idempotency-aware
if idempotency_key in IDEMPOTENCY_STORE: # => co-18: a REPLAY -- the key was already used
return Response(status=200, body=IDEMPOTENCY_STORE[idempotency_key]) # => the STORED response, verbatim
CHARGE_COUNT[0] += 1 # => co-18: the side effect ONLY runs on a genuinely new key
body: dict[str, object] = {"id": CHARGE_COUNT[0], "amount": amount, "status": "succeeded"} # => the real charge
IDEMPOTENCY_STORE[idempotency_key] = body # => records it for any FUTURE replay of this same key
return Response(status=201, body=body) # => 201 -- a freshly created charge
first = create_charge("key-abc-123", 5000) # => call 1: a genuinely new key
print(f"first call: status={first.status}, body={first.body}") # => Output: 201, charge_count=1
replay = create_charge("key-abc-123", 5000) # => call 2: the SAME key, resent (e.g. after a timeout)
# => replay.body == first.body -- the SAME dict object, not a newly minted one
print(f"replay call: status={replay.status}, body={replay.body}") # => Output: 200, IDENTICAL body
print(f"total charges actually created: {CHARGE_COUNT[0]}") # => Output: 1 -- co-18: applied exactly onceRun: python3 example.py
Output:
first call: status=201, body={'id': 1, 'amount': 5000, 'status': 'succeeded'}
replay call: status=200, body={'id': 1, 'amount': 5000, 'status': 'succeeded'}
total charges actually created: 1Key takeaway: CHARGE_COUNT[0] ends at 1, not 2 -- despite create_charge being called
twice with the same key, the second call's status is 200 (not 201) and returns the stored body
verbatim, proving the underlying charge happened exactly once.
Why it matters: This is the entire point of Example 37's recorded key: a client that legitimately does not know whether its first request actually reached the server (a timed-out connection, a dropped response) can retry with the SAME key and get the SAME outcome back, safely, instead of risking a second real charge.
Example 39: Reusing an Idempotency-Key with a Different Body
ex-39 · exercises co-18
Stripe's own prior art rejects a replayed key whose BODY does not match the original request -- a key means "this exact operation, retried," not "any operation, deduplicated by key alone."
learning/code/ex-39-idempotency-key-mismatch/example.py
# pyright: strict
"""Example 39: Reusing an Idempotency-Key with a Different Body. (co-18)
Stripe's own prior art rejects a replayed key whose BODY does not match the
original request -- a key means "this exact operation, retried," not "any
operation, deduplicated by key alone."
"""
from dataclasses import dataclass # => a small typed response record for this example
IDEMPOTENCY_STORE: dict[str, dict[str, object]] = {} # => co-18: key -> (original request body, response)
@dataclass # => co-18: status plus either the resource or a mismatch error
class Response: # => co-18: a mismatch is reported the same way any other rejection is
status: int # => the HTTP status code
body: dict[str, object] # => the charge, or a mismatch error description
def create_charge(idempotency_key: str, amount: int) -> Response: # => POST /charges, mismatch-aware
if idempotency_key in IDEMPOTENCY_STORE: # => co-18: this key has been used before
stored = IDEMPOTENCY_STORE[idempotency_key] # => the ORIGINAL request+response pairing
if stored["amount"] != amount: # => co-18: the body genuinely DIFFERS from the original
mismatch_error: dict[str, object] = {"error": "idempotency key reused with a different request body"}
return Response(422, mismatch_error) # => co-18: rejected -- the key means THIS exact body
return Response(200, stored["response"]) # type: ignore[arg-type] # => co-18: a genuine replay
body: dict[str, object] = {"id": 1, "amount": amount, "status": "succeeded"} # => explicit dict[str, object]
IDEMPOTENCY_STORE[idempotency_key] = {"amount": amount, "response": body} # => records BOTH facts
return Response(status=201, body=body) # => 201 -- a freshly created charge
first = create_charge("key-xyz-789", 1000) # => call 1: a new key, amount 1000
print(f"first call: status={first.status}, body={first.body}") # => Output: 201, amount 1000
mismatch = create_charge("key-xyz-789", 2000) # => call 2: SAME key, DIFFERENT amount -- co-18's guard
# => mismatch.status is 422 -- the key alone was never enough to deduplicate this call
print(f"mismatch call: status={mismatch.status}, body={mismatch.body}") # => Output: 422, rejected
genuine_replay = create_charge("key-xyz-789", 1000) # => call 3: SAME key, SAME amount -- a true replay
print(f"replay call: status={genuine_replay.status}, body={genuine_replay.body}") # => Output: 200, replayedRun: python3 example.py
Output:
first call: status=201, body={'id': 1, 'amount': 1000, 'status': 'succeeded'}
mismatch call: status=422, body={'error': 'idempotency key reused with a different request body'}
replay call: status=200, body={'id': 1, 'amount': 1000, 'status': 'succeeded'}Key takeaway: The SAME key with a DIFFERENT amount is rejected (422), while the SAME key with
the SAME amount is accepted as a genuine replay (200) -- the key alone is not enough to
deduplicate; it must be paired with an unchanged request body.
Why it matters: Without this guard, a client bug that reuses a key across two genuinely
different requests would silently discard the second request's real intent (or worse, apply the
first request's stale data) -- rejecting the mismatch surfaces the bug immediately, as an explicit
422, instead of a silent, hard-to-diagnose data-integrity problem.
Example 40: 429 Too Many Requests + Retry-After
ex-40 · exercises co-19
RFC 6585's 429 communicates a rate limit was exceeded; an optional Retry-After header tells the
caller how long to wait before trying again -- a simple fixed-budget counter is enough to
demonstrate the contract.
%% Color Palette: Orange #DE8F05, Teal #029E73, Brown #CA9161
graph LR
A["Budget: 3"] -->|calls 1-3: 200 OK| D["Budget: 0"]
D -->|call 4| E["429 + Retry-After: 60"]
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#000000,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
class A teal
class D orange
class E brown
learning/code/ex-40-rate-limit-429/example.py
# pyright: strict
"""Example 40: 429 Too Many Requests + Retry-After. (co-19)
RFC 6585's `429` communicates a rate limit was exceeded; an optional
`Retry-After` header tells the caller how long to wait before trying again
-- a simple fixed-budget counter is enough to demonstrate the contract.
"""
from dataclasses import dataclass, field # => field: default_factory for the headers dict
REQUEST_BUDGET = [3] # => a mutable counter cell -- 3 requests allowed before the limit trips
@dataclass # => co-19: status plus headers, so Retry-After is only present when relevant
class Response: # => co-19: an empty headers dict means "no Retry-After needed"
status: int # => the HTTP status code
headers: dict[str, str] = field(default_factory=dict[str, str]) # => carries Retry-After when limited
def call_api() -> Response: # => a single API call, gated by the shared budget above
if REQUEST_BUDGET[0] <= 0: # => co-19: the budget is exhausted
return Response(status=429, headers={"Retry-After": "60"}) # => co-19: 429 + how long to wait
REQUEST_BUDGET[0] -= 1 # => co-19: consumes one unit of budget on a successful call
return Response(status=200) # => a normal, successful call
results = [call_api() for _ in range(4)] # => co-19: 4 calls against a budget of 3 -- the 4th trips it
# => results has 4 elements; the first 3 are status 200, the 4th is status 429
for i, response in enumerate(results, start=1): # => print every call's own outcome
has_retry = "Retry-After" in response.headers # => True only for the 429 call
extra = f", Retry-After={response.headers['Retry-After']}" if has_retry else "" # => co-19: conditional
print(f"call {i}: status={response.status}{extra}") # => Output: 200,200,200, then 429 + Retry-AfterRun: python3 example.py
Output:
call 1: status=200
call 2: status=200
call 3: status=200
call 4: status=429, Retry-After=60Key takeaway: The first three calls consume the entire budget of 3 and succeed with 200; the
fourth call finds REQUEST_BUDGET[0] <= 0 and returns 429 with a Retry-After: 60 header -- the
exact call at which the limit trips is deterministic and visible.
Why it matters: 429 alone tells a caller "you were rejected"; Retry-After tells it "here is
when it is worth trying again" -- without that second piece, a well-behaved client is forced to
guess a backoff interval, while a poorly-behaved one just retries immediately and makes the
overload worse. Retry-After also protects the SERVER: without it, every rejected client tends to retry on its own arbitrary schedule, and those uncoordinated retries can pile up into a second overload right on top of the first.
Example 41: Structured RateLimit / RateLimit-Policy Headers
ex-41 · exercises co-20
draft-ietf-httpapi-ratelimit-headers-11 (active, unlike Example 39's lapsed idempotency draft)
standardizes RateLimit/RateLimit-Policy -- structured fields exposing remaining quota, replacing
ad-hoc X-RateLimit-*.
learning/code/ex-41-ratelimit-headers/example.py
# pyright: strict
"""Example 41: Structured RateLimit / RateLimit-Policy Headers. (co-20)
`draft-ietf-httpapi-ratelimit-headers-11` (active, unlike Example 39's
lapsed idempotency draft) standardizes `RateLimit`/`RateLimit-Policy` --
structured fields exposing remaining quota, replacing ad-hoc `X-RateLimit-*`.
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-20: status plus the structured rate-limit headers
class Response: # => co-20: both header fields ride alongside a normal, successful status
status: int # => the HTTP status code
headers: dict[str, str] # => carries the structured RateLimit fields
def call_api(remaining: int, limit: int, window_seconds: int) -> Response: # => one API call, quota-aware
return Response( # => co-20: exposes the CURRENT quota state on every call, success or not
status=200, # => this example models a successful call still within budget
headers={ # => the two draft-defined structured header fields
"RateLimit": f"limit={limit}, remaining={remaining}", # => co-20: this call's own quota state
"RateLimit-Policy": f"{limit};w={window_seconds}", # => co-20: the POLICY -- limit + window
}, # => end of the headers dict
) # => end of the Response construction
response = call_api(remaining=42, limit=100, window_seconds=3600) # => a call with quota still available
print(f"status={response.status}") # => Output: status=200
print(f"RateLimit={response.headers['RateLimit']}") # => Output: limit=100, remaining=42
print(f"RateLimit-Policy={response.headers['RateLimit-Policy']}") # => Output: 100;w=3600
def parse_remaining(headers: dict[str, str]) -> int: # => co-20: a client parsing the structured field
ratelimit_value = headers["RateLimit"] # => the raw header string
remaining_part = ratelimit_value.split("remaining=")[1] # => co-20: extracts just the number
return int(remaining_part) # => co-20: a client can react to "how much quota is left"
parsed = parse_remaining(response.headers) # => co-20: a client parses the header back into a number
# => parsed is 42 (type: int) -- matches what the server set exactly
print(f"parsed remaining: {parsed}") # => Output: 42 -- matches what the server setRun: python3 example.py
Output:
status=200
RateLimit=limit=100, remaining=42
RateLimit-Policy=100;w=3600
parsed remaining: 42Key takeaway: RateLimit-Policy states the fixed policy (100 requests per 3600-second window),
while RateLimit states the caller's CURRENT position within that policy (42 remaining) -- two
distinct facts, both present on every response, not just on a rejection.
Why it matters: Unlike Example 40's 429, which only appears once the limit is already
exceeded, exposing RateLimit/RateLimit-Policy on every SUCCESSFUL call lets a well-behaved
client self-throttle before hitting the wall, since it can see its own remaining quota after every
single request. This shifts rate limiting from a reactive "wait for the 429, then back off" pattern to a proactive one, where a well-behaved client paces its own requests before ever touching the limit.
Example 42: The Remaining Counter Decrements to Zero
ex-42 · exercises co-20
Following Example 41's structured header, this example shows the FULL lifecycle: remaining
decrements per call until it reaches zero, at which point the NEXT call is the one Example 40's
429 would reject.
learning/code/ex-42-quota-remaining/example.py
# pyright: strict
"""Example 42: The Remaining Counter Decrements to Zero. (co-20)
Following Example 41's structured header, this example shows the FULL
lifecycle: `remaining` decrements per call until it reaches zero, at which
point the NEXT call is the one Example 40's `429` would reject.
"""
QUOTA = {"limit": 3, "remaining": 3} # => co-20: a small quota, easy to exhaust in one script run
# => QUOTA is {'limit': 3, 'remaining': 3} (type: dict[str, int]) before any call runs
def call_api() -> dict[str, int]: # => one call against the shared quota above
QUOTA["remaining"] -= 1 # => co-20: every call decrements the remaining counter by exactly one
return dict(QUOTA) # => a snapshot of the quota state AFTER this call
for call_number in range(1, 4): # => co-20: three calls, exactly draining the quota to zero
state = call_api() # => makes one call, observing the quota afterward
print(f"call {call_number}: remaining={state['remaining']}") # => Output: 2, 1, 0
print(f"quota exhausted: {QUOTA['remaining'] == 0}") # => Output: True -- co-20: the NEXT call trips 429Run: python3 example.py
Output:
call 1: remaining=2
call 2: remaining=1
call 3: remaining=0
quota exhausted: TrueKey takeaway: remaining reaches exactly 0 after the third call of a limit-3 quota, and
quota_exhausted confirms it -- this is precisely the boundary at which Example 40's fourth call
would receive 429 instead of 200.
Why it matters: Seeing the decrement happen call-by-call makes the connection between Example
41's per-call header and Example 40's eventual rejection concrete: RateLimit's remaining field
is not a static number -- it is a live countdown a client can watch approach zero and react to
before actually hitting the limit. A client dashboard built on this live countdown can warn a caller well before the limit is reached, rather than surprising them with a rejected request they had no way to anticipate.
Example 43: ETag + If-None-Match -> 304 Not Modified
ex-43 · exercises co-22
The server computes an ETag (a content hash); a client resending it via If-None-Match gets 304 Not Modified with an EMPTY body when the content has not changed -- saving the bandwidth of
re-sending unchanged data.
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /articles/1 (no If-None-Match yet)
Server-->>Client: 200 OK, body + ETag: v1-abc123
Client->>Server: GET /articles/1, If-None-Match: v1-abc123
Server-->>Client: 304 Not Modified, EMPTY body
learning/code/ex-43-etag-304/example.py
# pyright: strict
"""Example 43: ETag + If-None-Match -> 304 Not Modified. (co-22)
The server computes an `ETag` (a content hash); a client resending it via
`If-None-Match` gets `304 Not Modified` with an EMPTY body when the content
has not changed -- saving the bandwidth of re-sending unchanged data.
"""
import hashlib # => stdlib: computes the ETag as a content hash
from dataclasses import dataclass # => a small typed response record for this example
ARTICLE_BODY = '{"id": 1, "title": "Hello"}' # => the resource's current serialized content
def compute_etag(body: str) -> str: # => co-22: a deterministic hash of the CURRENT content
return hashlib.sha256(body.encode()).hexdigest()[:12] # => a short, stable content fingerprint
@dataclass # => co-22: status, headers (the ETag), and a body that may be empty on a 304
class Response:
status: int # => the HTTP status code
headers: dict[str, str] # => carries the ETag
body: str # => empty on a 304, populated on a 200
def get_article(if_none_match: str | None) -> Response: # => GET /articles/1, conditional-aware
current_etag = compute_etag(ARTICLE_BODY) # => co-22: recomputed fresh on every call
if if_none_match == current_etag: # => co-22: the client's cached copy is STILL valid
return Response(304, {"ETag": current_etag}, "") # => 304 -- no need to resend the body
return Response(200, {"ETag": current_etag}, ARTICLE_BODY) # => 200 -- full body, plus the ETag
first = get_article(if_none_match=None) # => request 1: no cached copy yet
print(f"first: status={first.status}, body={first.body!r}") # => Output: 200, full body
cached_etag = first.headers["ETag"] # => the client REMEMBERS this ETag for next time
second = get_article(if_none_match=cached_etag) # => request 2: resends the SAME ETag
# => second.body is "" -- the server saved the bandwidth of resending unchanged content
print(f"second: status={second.status}, body={second.body!r}") # => Output: 304, empty body -- unchangedRun: python3 example.py
Output:
first: status=200, body='{"id": 1, "title": "Hello"}'
second: status=304, body=''Key takeaway: The second request sends back its own ETag from the first response, and because
ARTICLE_BODY never changed, get_article returns 304 with an empty body -- the client already
has the correct data, so the server sends nothing further.
Why it matters: ETag/If-None-Match lets the server confirm "your cached copy is still
correct" for the cost of just a hash comparison, instead of re-serializing and re-transmitting the
full resource on every request -- a meaningful saving for large or frequently-polled resources. For a resource fetched thousands of times a day but changed only occasionally, that saved serialization and transfer cost adds up to a meaningful reduction in both server load and client latency.
Example 44: The Cache-Control: max-age Directive
ex-44 · exercises co-22
Cache-Control tells the CLIENT (or an intermediary cache) how long a response may be reused
WITHOUT even asking the server -- distinct from ETag's "ask, and I'll tell you if it changed"
(Example 43).
learning/code/ex-44-cache-control-header/example.py
# pyright: strict
"""Example 44: The Cache-Control: max-age Directive. (co-22)
`Cache-Control` tells the CLIENT (or an intermediary cache) how long a
response may be reused WITHOUT even asking the server -- distinct from
`ETag`'s "ask, and I'll tell you if it changed" (Example 43).
"""
from dataclasses import dataclass # => a small typed response record for this example
@dataclass # => co-22: status, headers (the caching directive), and the body
class Response: # => co-22: max-age is a header, not a change to the status or body shape
status: int # => the HTTP status code
headers: dict[str, str] # => carries the Cache-Control directive
body: dict[str, object] # => the resource's own data
def get_article() -> Response: # => GET /articles/1 -- a response that is safe to cache briefly
return Response( # => co-22: max-age=60 means "reusable for 60 seconds, no request needed"
status=200, # => a normal successful GET
headers={"Cache-Control": "max-age=60"}, # => co-22: the caching directive itself
body={"id": 1, "title": "Hello"}, # => the resource's own data
) # => end of the Response construction
def parse_max_age(cache_control: str) -> int: # => co-22: a client parsing the directive's own value
directive_value = cache_control.split("max-age=")[1] # => extracts just the number part
return int(directive_value) # => co-22: how many seconds the client may reuse this response
response = get_article() # => run the handler
print(f"Cache-Control={response.headers['Cache-Control']}") # => Output: Cache-Control=max-age=60
max_age_seconds = parse_max_age(response.headers["Cache-Control"]) # => co-22: parses it back to an int
# => max_age_seconds is 60 (type: int) -- the client may skip a full round trip for this long
print(f"parsed max_age: {max_age_seconds} seconds") # => Output: parsed max_age: 60 secondsRun: python3 example.py
Output:
Cache-Control=max-age=60
parsed max_age: 60 secondsKey takeaway: max-age=60 is a promise the server makes UP FRONT -- for 60 seconds, the client
may reuse this exact response without contacting the server at all, no round trip and no
If-None-Match check required, unlike Example 43's ETag flow.
Why it matters: Cache-Control and ETag solve overlapping but distinct problems: max-age
avoids the round trip entirely for a bounded window, while ETag still requires a round trip but
avoids re-sending the BODY when nothing changed -- a well-designed API typically uses both, layered
together. Relying on only one of the two leaves a gap: max-age alone risks serving genuinely stale data inside its window, while ETag alone still pays a round trip on every single request.
Example 45: Optimistic Concurrency with If-Match
ex-45 · exercises co-22
If-Match requires a PUT's supplied ETag to match the CURRENT one before the write is allowed -- a
stale write (one based on outdated data) is rejected with 412 Precondition Failed instead of
silently overwriting.
sequenceDiagram
participant Client
participant Server
Note over Server: ARTICLE.etag = v1-abc123
Client->>Server: PUT /articles/1, If-Match: v1-WRONG
Server-->>Client: 412 Precondition Failed (stale write rejected)
Client->>Server: PUT /articles/1, If-Match: v1-abc123
Server-->>Client: 200 OK, etag advances to v2-def456
learning/code/ex-45-conditional-put-if-match/example.py
# pyright: strict
"""Example 45: Optimistic Concurrency with If-Match. (co-22)
`If-Match` requires a PUT's supplied ETag to match the CURRENT one before
the write is allowed -- a stale write (one based on outdated data) is
rejected with `412 Precondition Failed` instead of silently overwriting.
"""
from dataclasses import dataclass # => a small typed response record for this example
ARTICLE = {"title": "Original title", "etag": "v1-abc123"} # => co-22: the resource carries its own ETag
@dataclass # => co-22: status plus a small body describing the outcome
class Response:
status: int # => the HTTP status code
body: dict[str, str] # => the updated resource, or an error description
def put_article(new_title: str, if_match: str) -> Response: # => PUT /articles/1, optimistic-concurrency-aware
if if_match != ARTICLE["etag"]: # => co-22: the caller's ETag does NOT match the CURRENT one
return Response(412, {"error": "Precondition Failed: resource has changed"}) # => 412, rejected
ARTICLE["title"] = new_title # => co-22: the write is only applied AFTER the ETag check passes
ARTICLE["etag"] = "v2-def456" # => co-22: a successful write ALSO advances the ETag
return Response(200, {"title": ARTICLE["title"], "etag": ARTICLE["etag"]}) # => 200, the new state
stale_write = put_article("Stale update", if_match="v1-WRONG") # => a caller with an OUTDATED etag
# => ARTICLE["title"] is still "Original title" -- the stale write never actually applied
print(f"stale write: status={stale_write.status}, body={stale_write.body}") # => Output: 412, rejected
fresh_write = put_article("Fresh update", if_match="v1-abc123") # => a caller with the CURRENT etag
print(f"fresh write: status={fresh_write.status}, body={fresh_write.body}") # => Output: 200, appliedRun: python3 example.py
Output:
stale write: status=412, body={'error': 'Precondition Failed: resource has changed'}
fresh write: status=200, body={'title': 'Fresh update', 'etag': 'v2-def456'}Key takeaway: stale_write (with a wrong If-Match value) never reaches ARTICLE["title"] = new_title -- the check runs BEFORE any mutation, so a stale write cannot silently clobber a change
made by someone else since the caller last read the resource.
Why it matters: Without optimistic concurrency, two clients editing the same resource "last
write wins" -- whoever's PUT lands second silently discards the first client's edit with no error
at all. If-Match converts that silent data loss into an explicit, catchable 412, giving the
second client a chance to re-fetch and retry. Without it, two people editing the same document in two browser tabs would never know one of their edits vanished -- 412 makes that collision visible the moment it happens, instead of hiding it forever.
Example 46: Authorization: Bearer
ex-46 · exercises co-23
RFC 6750 defines the Authorization: Bearer <token> syntax carrying an OAuth 2.0 (RFC 6749) access
token -- a request with NO token is rejected with 401 Unauthorized, distinct from Example 48's
scope-based 403.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A[Authorization header] --> B{present?}
B -->|no| C["401: missing header"]
B -->|yes| D{starts with Bearer ?}
D -->|no| E["401: expected Bearer"]
D -->|yes| F{token known?}
F -->|no| G["401: invalid token"]
F -->|yes| H{has required scope?}
H -->|no, Example 48| I["403: missing scope"]
H -->|yes| J["200 OK"]
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#000000,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#000000,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
class A blue
class B,D,F,H orange
class C,E,G brown
class I purple
class J teal
learning/code/ex-46-bearer-token-auth/example.py
# pyright: strict
"""Example 46: Authorization: Bearer <token>. (co-23)
RFC 6750 defines the `Authorization: Bearer <token>` syntax carrying an
OAuth 2.0 (RFC 6749) access token -- a request with NO token is rejected
with `401 Unauthorized`, distinct from Example 48's scope-based `403`.
"""
from dataclasses import dataclass # => a small typed response record for this example
VALID_TOKENS: set[str] = {"token-abc-123"} # => co-23: the set of currently-valid bearer tokens
@dataclass # => co-23: status plus a small body describing the outcome
class Response:
status: int # => the HTTP status code
body: dict[str, str] # => the resource, or an authentication error description
def get_article(authorization_header: str | None) -> Response: # => GET /articles/1, auth-gated
if authorization_header is None: # => co-23: NO Authorization header at all
return Response(401, {"error": "missing Authorization header"}) # => 401, rejected outright
if not authorization_header.startswith("Bearer "): # => co-23: the WRONG scheme (not "Bearer")
return Response(401, {"error": "expected a Bearer token"}) # => 401, malformed scheme
token = authorization_header.removeprefix("Bearer ") # => co-23: RFC 6750's exact syntax, unpacked
if token not in VALID_TOKENS: # => co-23: a syntactically valid but UNKNOWN token
return Response(401, {"error": "invalid token"}) # => 401, still an authentication failure
return Response(200, {"id": "1", "title": "Hello"}) # => 200, a genuinely valid bearer token
missing = get_article(authorization_header=None) # => request 1: no header at all
print(f"missing: status={missing.status}, body={missing.body}") # => Output: 401
valid = get_article(authorization_header="Bearer token-abc-123") # => request 2: RFC 6750 syntax, valid token
# => all three rejection paths return 401 -- co-23 never leaks WHICH check failed to the caller
print(f"valid: status={valid.status}, body={valid.body}") # => Output: 200Run: python3 example.py
Output:
missing: status=401, body={'error': 'missing Authorization header'}
valid: status=200, body={'id': '1', 'title': 'Hello'}Key takeaway: get_article checks three distinct failure modes in order -- missing header,
wrong scheme, unknown token -- and all three collapse to the same 401; only a syntactically
correct header carrying a token in VALID_TOKENS reaches 200.
Why it matters: 401 Unauthorized specifically means "who you are is unverified or unproven" --
this example establishes the baseline authentication check every protected endpoint needs, before
Example 48 layers a SEPARATE authorization concern (what an authenticated caller is allowed to do)
on top of it. Conflating the two checks into one status code would leave a client unable to tell "log in again" from "you are logged in but not allowed here," two very different remediation paths.
Example 47: The X-API-Key Header
ex-47 · exercises co-23
An API key is a simpler alternative to OAuth's bearer token -- a single, long-lived secret carried
in a custom header (X-API-Key), checked against a known set; an INVALID key is rejected the same
way Example 46's does.
learning/code/ex-47-api-key-auth/example.py
# pyright: strict
"""Example 47: The X-API-Key Header. (co-23)
An API key is a simpler alternative to OAuth's bearer token -- a single,
long-lived secret carried in a custom header (`X-API-Key`), checked against
a known set; an INVALID key is rejected the same way Example 46's does.
"""
from dataclasses import dataclass # => a small typed response record for this example
VALID_API_KEYS: set[str] = {"key-live-abcdef"} # => co-23: the set of currently-valid API keys
@dataclass # => co-23: status plus a small body describing the outcome
class Response:
status: int # => the HTTP status code
body: dict[str, str] # => the resource, or an authentication error description
def get_article(x_api_key: str | None) -> Response: # => GET /articles/1, API-key-gated
if x_api_key is None: # => co-23: no X-API-Key header at all
return Response(401, {"error": "missing X-API-Key header"}) # => 401, rejected outright
if x_api_key not in VALID_API_KEYS: # => co-23: a KEY was supplied, but it is not a KNOWN one
return Response(401, {"error": "invalid API key"}) # => 401, an invalid credential
return Response(200, {"id": "1", "title": "Hello"}) # => 200, a genuinely valid key
invalid = get_article(x_api_key="key-wrong-999999") # => request 1: a key, but the WRONG one
print(f"invalid: status={invalid.status}, body={invalid.body}") # => Output: 401
valid = get_article(x_api_key="key-live-abcdef") # => request 2: the correct, known key
# => an API key is simpler than a bearer token to check, but it never expires on its own
print(f"valid: status={valid.status}, body={valid.body}") # => Output: 200Run: python3 example.py
Output:
invalid: status=401, body={'error': 'invalid API key'}
valid: status=200, body={'id': '1', 'title': 'Hello'}Key takeaway: A syntactically well-formed key that is simply not in VALID_API_KEYS produces
the exact same 401 as no key at all -- from the caller's perspective, "wrong key" and "no key" are
indistinguishable failures, which is deliberate: revealing which is true would leak information to
an attacker probing for valid keys.
Why it matters: API keys trade OAuth's richer token lifecycle (expiry, refresh, scoped grants -- Examples 46 and 48) for operational simplicity -- a single static secret a service-to-service integration can hold indefinitely, at the cost of no built-in expiration or fine-grained scoping unless the API layers that on top itself. This is exactly why API keys remain common for internal service-to-service calls behind a firewall, while OAuth dominates for anything exposed to third-party developers or end users directly.
Example 48: Scope-Gated Authorization
ex-48 · exercises co-23
OAuth scopes are space-delimited, case-sensitive strings (RFC 6749). A token can be VALID (Example 46) but still lack the SPECIFIC scope an operation requires -- that gap is 403 Forbidden, not 401 Unauthorized.
learning/code/ex-48-scope-check/example.py
# pyright: strict
"""Example 48: Scope-Gated Authorization. (co-23)
OAuth scopes are space-delimited, case-sensitive strings (RFC 6749). A
token can be VALID (Example 46) but still lack the SPECIFIC scope an
operation requires -- that gap is `403 Forbidden`, not `401 Unauthorized`.
"""
from dataclasses import dataclass # => a small typed response record for this example
TOKEN_SCOPES: dict[str, set[str]] = { # => co-23: token -> the space-delimited scopes it actually carries
"token-reader": {"articles:read"}, # => can read, but nothing else
"token-writer": {"articles:read", "articles:write"}, # => can read AND write
} # => end of TOKEN_SCOPES
@dataclass # => co-23: status plus a small body describing the outcome
class Response:
status: int # => the HTTP status code
body: dict[str, str] # => the resource, or an authorization error description
def delete_article(token: str, required_scope: str) -> Response: # => DELETE /articles/1, scope-gated
scopes = TOKEN_SCOPES.get(token, set()) # => co-23: the token's OWN granted scopes, if any
if required_scope not in scopes: # => co-23: a VALID token, but missing the SPECIFIC scope needed
return Response(403, {"error": f"missing required scope {required_scope!r}"}) # => 403, forbidden
return Response(204, {}) # => 204 -- the scope check passed, the delete succeeded (co-07's shape)
out_of_scope = delete_article("token-reader", required_scope="articles:write") # => a reader, needs write
# => 403, not 401 -- the token itself is perfectly valid, it just lacks this one scope
print(f"out of scope: status={out_of_scope.status}, body={out_of_scope.body}") # => Output: 403
in_scope = delete_article("token-writer", required_scope="articles:write") # => a writer, needs write
print(f"in scope: status={in_scope.status}, body={in_scope.body}") # => Output: 204Run: python3 example.py
Output:
out of scope: status=403, body={'error': "missing required scope 'articles:write'"}
in scope: status=204, body={}Key takeaway: Both tokens are equally VALID (neither triggers Example 46's 401) -- the
distinction here is purely about what each one is ALLOWED to do, and token-reader simply never
carries "articles:write" in its own granted scope set.
Key distinction (401 vs 403): 401 means "I don't know who you are" (Example 46); 403 means
"I know exactly who you are, and you are not allowed to do this" -- conflating the two hides useful
information from legitimate callers trying to diagnose their own integration.
Why it matters: Scopes let a single API expose fine-grained permissions (read vs. write, per resource type) without minting a separate token type for every combination -- a client requests only the scopes it needs, and the server enforces each operation's own scope requirement independently, which is exactly what Example 49 declares in the OpenAPI contract itself.
Example 49: Declaring bearer/apiKey Security Schemes in OpenAPI
ex-49 · exercises co-34
Auth (Examples 46-48) belongs in the CONTRACT, not just the handler code -- OpenAPI's
securitySchemes declares each scheme ONCE, and an operation's own security field states which
scheme(s) it requires.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A[components.securitySchemes] --> B["bearerAuth: http/bearer"]
A --> C["apiKeyAuth: apiKey/header"]
D["DELETE /articles/{id}.security"] -->|references| B
D -->|requires scope| E["articles:write"]
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#000000,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#000000,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
class A blue
class B orange
class C teal
class D purple
class E brown
learning/code/ex-49-openapi-security-scheme/example.py
# pyright: strict
"""Example 49: Declaring bearer/apiKey Security Schemes in OpenAPI. (co-34)
Auth (Examples 46-48) belongs in the CONTRACT, not just the handler code --
OpenAPI's `securitySchemes` declares each scheme ONCE, and an operation's
own `security` field states which scheme(s) it requires.
"""
from typing import Any # => the spec is arbitrary nested JSON
BEARER_SCHEME = {"type": "http", "scheme": "bearer"} # => co-34: matches Example 46's RFC 6750 syntax
API_KEY_SCHEME = {"type": "apiKey", "in": "header", "name": "X-API-Key"} # => co-34: matches Example 47
SECURITY_SCHEMES = {"bearerAuth": BEARER_SCHEME, "apiKeyAuth": API_KEY_SCHEME} # => co-34: registered by name
DELETE_OPERATION = {"security": [{"bearerAuth": ["articles:write"]}]} # => co-34: scheme name + required scope
SPEC: dict[str, Any] = { # => co-34: security schemes declared under components, referenced by name
"components": {"securitySchemes": SECURITY_SCHEMES}, # => the ONE place every scheme is defined
"paths": {"/articles/{id}": {"delete": DELETE_OPERATION}}, # => the operation that USES one scheme
} # => end of SPEC
def declared_schemes(spec: dict[str, Any]) -> list[str]: # => co-34: every scheme name the spec registers
return list(spec["components"]["securitySchemes"].keys()) # => the two names declared above
def operation_requirement(spec: dict[str, Any]) -> dict[str, list[str]]: # => co-34: what ONE op requires
delete_op = spec["paths"]["/articles/{id}"]["delete"] # => the specific operation to inspect
return delete_op["security"][0] # => co-34: the first (and here, only) security requirement
print(f"declared schemes: {declared_schemes(SPEC)}") # => Output: ['bearerAuth', 'apiKeyAuth']
print(f"DELETE requires: {operation_requirement(SPEC)}") # => Output: {'bearerAuth': ['articles:write']}
# => a client (or a codegen tool) can now discover the auth requirement WITHOUT reading handler codeRun: python3 example.py
Output:
declared schemes: ['bearerAuth', 'apiKeyAuth']
DELETE requires: {'bearerAuth': ['articles:write']}Key takeaway: components.securitySchemes names each scheme exactly once (bearerAuth,
apiKeyAuth), and the DELETE operation references bearerAuth by name with a required scope --
neither the scheme's own definition nor its enforcement logic is duplicated per operation.
Why it matters: Declaring auth in the contract, not just in handler code, lets generated tooling (client SDKs, mock servers, API documentation -- co-10's OpenAPI toolchain) show a caller exactly which credentials and scopes an operation needs BEFORE they write a single line of integration code, instead of discovering it by triggering Example 46's or Example 48's rejection.
Example 50: ?fields= Selects a Subset of Returned Fields
ex-50 · exercises co-31
?fields=name,email lets a caller ask for only the fields it needs -- fewer bytes on the wire, and
no need to discard unused data the way a full-record response would force the caller to.
learning/code/ex-50-partial-response-fields/example.py
# pyright: strict
"""Example 50: ?fields= Selects a Subset of Returned Fields. (co-31)
`?fields=name,email` lets a caller ask for only the fields it needs --
fewer bytes on the wire, and no need to discard unused data the way a
full-record response would force the caller to.
"""
FULL_ARTICLE = { # => co-31: the FULL resource, before any field selection is applied
"id": 1, # => always small, always included
"title": "Hello, API Design", # => the field a caller might actually want
"body": "A very long article body..." * 20, # => large -- exactly what field selection lets a caller skip
"author": "Ada", # => another optional field
} # => end of FULL_ARTICLE
def get_article(fields: str | None) -> dict[str, object]: # => GET /articles/1?fields=
if fields is None: # => co-31: no selection given -- return everything
return dict(FULL_ARTICLE) # => a full copy of the resource
requested = fields.split(",") # => co-31: parses the comma-separated field list
return {key: FULL_ARTICLE[key] for key in requested if key in FULL_ARTICLE} # => co-31: only those fields
full = get_article(fields=None) # => request 1: no selection -- gets everything
print(f"full response has {len(full)} fields: {sorted(full.keys())}") # => Output: all 4 fields
filtered = get_article(fields="id,title") # => request 2: asks for ONLY id and title
# => filtered has 2 keys, not 4 -- the large "body" field never crosses the wire at all
print(f"filtered response: {filtered}") # => Output: {'id': 1, 'title': 'Hello, API Design'} -- co-31Run: python3 example.py
Output:
full response has 4 fields: ['author', 'body', 'id', 'title']
filtered response: {'id': 1, 'title': 'Hello, API Design'}Key takeaway: Requesting fields="id,title" returns exactly those two keys -- notably
excluding "body", the field the resource comment marks as large, which is precisely the field a
bandwidth-conscious caller would want to skip.
Why it matters: A resource with a large or rarely-needed field (an article's full body, a
user's full activity history) forces every caller into an all-or-nothing trade-off without field
selection -- ?fields= lets each caller independently choose the shape it actually needs, from the
SAME underlying endpoint. Without it, a mobile client paying for every byte over a metered connection has no way to opt out of fields it will never render, even though the server already has that data cheaply available.
Example 51: JSON:API Sparse Fieldsets
ex-51 · exercises co-31, co-28
JSON:API's fields[type]= parameter selects a sparse fieldset the SAME way Example 50 does, but
wraps the result in JSON:API's own data/type/id/attributes envelope -- co-28's standardized
hypermedia shape.
learning/code/ex-51-sparse-fieldset-jsonapi/example.py
# pyright: strict
"""Example 51: JSON:API Sparse Fieldsets. (co-31, co-28)
JSON:API's `fields[type]=` parameter selects a sparse fieldset the SAME way
Example 50 does, but wraps the result in JSON:API's own
`data`/`type`/`id`/`attributes` envelope -- co-28's standardized hypermedia shape.
"""
FULL_ATTRIBUTES = {"title": "Hello, API Design", "body": "A long body...", "author": "Ada"}
# => co-31: the full attribute set, before sparse-fieldset selection
def jsonapi_response(resource_id: str, resource_type: str, fields: str | None) -> dict[str, object]:
# => GET /articles/1?fields[articles]=title -- co-28: the JSON:API envelope shape
attributes = FULL_ATTRIBUTES # => defaults to every attribute
if fields is not None: # => co-31: a sparse fieldset was explicitly requested
requested = fields.split(",") # => parses the comma-separated attribute list
attributes = {k: v for k, v in FULL_ATTRIBUTES.items() if k in requested} # => co-31: only those
return { # => co-28: the standardized JSON:API top-level envelope
"data": {"type": resource_type, "id": resource_id, "attributes": attributes} # => co-28's own shape
} # => end of the envelope
full = jsonapi_response("1", "articles", fields=None) # => request 1: no sparse fieldset
print(f"full: {full}") # => Output: all three attributes, wrapped in the JSON:API envelope
sparse = jsonapi_response("1", "articles", fields="title") # => request 2: only "title" requested
# => sparse["data"]["attributes"] has exactly 1 key -- the top-level envelope shape never changes
print(f"sparse: {sparse}") # => Output: only title, still wrapped in the SAME envelope shapeRun: python3 example.py
Output:
full: {'data': {'type': 'articles', 'id': '1', 'attributes': {'title': 'Hello, API Design', 'body': 'A long body...', 'author': 'Ada'}}}
sparse: {'data': {'type': 'articles', 'id': '1', 'attributes': {'title': 'Hello, API Design'}}}Key takeaway: Both calls return the identical data/type/id envelope structure -- only the
nested attributes object shrinks when fields narrows the selection, because JSON:API standardizes
the ENVELOPE separately from whatever fieldset it happens to carry.
Why it matters: Example 50's ad-hoc ?fields= and this example's fields[type]= solve the same
problem, but JSON:API pairs field selection with a FIXED response shape (data.type, data.id,
data.attributes) every resource in the API shares -- a client library written once against that
shape works for every resource type, not just articles. That reusability is JSON:API's whole pitch: a generic client library, written once against the spec, works unmodified against any conformant API, with no per-endpoint custom parsing code required.
Example 52: POST /batch -- N Sub-Operations, One Request
ex-52 · exercises co-32
A batch endpoint applies N distinct sub-operations in one HTTP round trip, dispatching each to its own handler and returning an array of INDIVIDUAL sub-results -- one sub-operation failing does not fail the others.
learning/code/ex-52-batch-endpoint/example.py
# pyright: strict
"""Example 52: POST /batch -- N Sub-Operations, One Request. (co-32)
A batch endpoint applies N distinct sub-operations in one HTTP round trip,
dispatching each to its own handler and returning an array of INDIVIDUAL
sub-results -- one sub-operation failing does not fail the others.
"""
from typing import Any, Callable # => a handler is just a function, typed for clarity
STORE: dict[int, str] = {1: "Existing article"} # => the resource store the batch operates against
def handle_get(article_id: int) -> dict[str, Any]: # => one possible sub-operation
if article_id not in STORE: # => this specific sub-op can fail on its own
return {"status": 404, "body": {"error": "not found"}} # => 404, but only for THIS sub-op
return {"status": 200, "body": {"id": article_id, "title": STORE[article_id]}} # => 200, found
HANDLERS: dict[str, Callable[[int], dict[str, Any]]] = {"get": handle_get} # => co-32: op name -> handler
def run_batch(operations: list[dict[str, Any]]) -> list[dict[str, Any]]: # => POST /batch
results: list[dict[str, Any]] = [] # => co-32: one sub-result per sub-operation, in order
for op in operations: # => co-32: dispatches EACH sub-operation independently
handler = HANDLERS[op["method"]] # => co-32: looks up the right handler for this sub-op
results.append(handler(op["article_id"])) # => co-32: a failure here does not stop the loop
return results # => the full array of individual sub-results
batch_request = [ # => co-32: three sub-operations, one of which targets a missing resource
{"method": "get", "article_id": 1}, # => sub-op 1: found
{"method": "get", "article_id": 999}, # => sub-op 2: NOT found -- fails independently
{"method": "get", "article_id": 1}, # => sub-op 3: found, unaffected by sub-op 2's failure
]
results = run_batch(batch_request) # => co-32: runs all three in one call
# => results has 3 entries: status 200, 404, 200 -- one failure did not abort the batch
for i, result in enumerate(results, start=1): # => print each sub-result on its own line
print(f"sub-op {i}: {result}") # => Output: 200, 404, 200 -- each independentRun: python3 example.py
Output:
sub-op 1: {'status': 200, 'body': {'id': 1, 'title': 'Existing article'}}
sub-op 2: {'status': 404, 'body': {'error': 'not found'}}
sub-op 3: {'status': 200, 'body': {'id': 1, 'title': 'Existing article'}}Key takeaway: Sub-operation 2's 404 has no effect on sub-operation 3 -- the for loop keeps
going and returns a full array of three independent results, each carrying its own status and body,
just as three separate HTTP requests would.
Why it matters: A naive "wrap N requests in one HTTP call" design might fail the whole batch if any one sub-operation fails; treating each sub-operation as independently successful or failed (the way this example does) preserves the exact same semantics a caller would get from N separate requests, while still saving N-1 round trips.
Example 53: Bulk-Creating Many Resources in One Request
ex-53 · exercises co-32
A single POST carrying a LIST of resources creates all of them in one round trip -- distinct from Example 52's mixed-operation batch, this is N creates of the SAME kind, verified by an id assigned per created item.
learning/code/ex-53-bulk-create/example.py
# pyright: strict
"""Example 53: Bulk-Creating Many Resources in One Request. (co-32)
A single POST carrying a LIST of resources creates all of them in one round
trip -- distinct from Example 52's mixed-operation batch, this is N creates
of the SAME kind, verified by an id assigned per created item.
"""
STORE: dict[int, dict[str, str]] = {} # => the resource store bulk-create writes into
NEXT_ID = [1] # => a mutable counter cell -- mints a fresh id per created item
def bulk_create_articles(titles: list[str]) -> list[dict[str, object]]: # => POST /articles/bulk
created: list[dict[str, object]] = [] # => co-32: one created record per input title
for title in titles: # => co-32: iterates the WHOLE input list in one call
new_id = NEXT_ID[0] # => a fresh id for this specific item
STORE[new_id] = {"title": title} # => writes it into the store
NEXT_ID[0] += 1 # => advances the counter for the NEXT item
created.append({"id": new_id, "title": title}) # => co-32: records what was actually created
return created # => the full list of created resources, one per input title
input_titles = ["First", "Second", "Third"] # => three titles submitted in ONE request
created = bulk_create_articles(input_titles) # => co-32: creates all three in a single call
# => created has 3 dicts, ids 1, 2, 3 -- one HTTP round trip produced three resources
print(f"created {len(created)} articles: {created}") # => Output: 3 articles, each with its own id
assert len(created) == len(input_titles) # => co-32: verifies created count matches input count exactly
assert {c["id"] for c in created} == {1, 2, 3} # => co-32: verifies each got a DISTINCT idRun: python3 example.py
Output:
created 3 articles: [{'id': 1, 'title': 'First'}, {'id': 2, 'title': 'Second'}, {'id': 3, 'title': 'Third'}]Key takeaway: Three titles submitted in one call become three DISTINCT resources, each with its
own auto-assigned id (1, 2, 3) -- the two assert statements confirm the count and the distinctness
of every created id, not just that "something" was returned.
Why it matters: Bulk create is the "same operation, many times, one round trip" counterpart to Example 52's "many DIFFERENT operations, one round trip" -- both save network overhead, but bulk create's simpler, uniform shape (a list of the SAME resource type) makes it easier for client libraries to generate and validate than a general-purpose batch envelope.
Example 54: Registering a Webhook Subscription
ex-54 · exercises co-33
A webhook subscription is itself an API resource: a caller POSTs a target URL and the event types it wants, and the API stores that subscription for future OUTBOUND delivery -- the foundation Example 55's signing builds on.
sequenceDiagram
participant Caller
participant API
participant Receiver as Caller's Endpoint
Caller->>API: POST /webhooks (url, event_types)
API-->>Caller: 201, subscription stored
Note over API: Example 55 -- HMAC-sign the outbound payload
API->>Receiver: POST url, body + X-Webhook-Signature
Receiver-->>Receiver: recompute HMAC, verify signature
learning/code/ex-54-webhook-subscribe/example.py
# pyright: strict
"""Example 54: Registering a Webhook Subscription. (co-33)
A webhook subscription is itself an API resource: a caller POSTs a target
URL and the event types it wants, and the API stores that subscription for
future OUTBOUND delivery -- the foundation Example 55's signing builds on.
"""
from dataclasses import dataclass, field # => field: default_factory for the event-types list
SUBSCRIPTIONS: dict[int, dict[str, object]] = {} # => co-33: id -> the stored subscription record
NEXT_ID = [1] # => a mutable counter cell -- mints a fresh subscription id
@dataclass # => co-33: what a caller submits to register a webhook
class WebhookSubscriptionRequest: # => co-33: the two facts a subscription needs to be delivered
url: str # => the caller's OWN endpoint, where events will be delivered
event_types: list[str] = field(default_factory=list[str]) # => which events this subscriber wants
def subscribe(request: WebhookSubscriptionRequest) -> dict[str, object]: # => POST /webhooks
subscription_id = NEXT_ID[0] # => a fresh id for this subscription
record: dict[str, object] = { # => co-33: the full record, explicitly dict[str, object]
"id": subscription_id, # => the subscription's own id
"url": request.url, # => where events will be delivered
"event_types": request.event_types, # => which events this subscriber wants
} # => end of the record dict
SUBSCRIPTIONS[subscription_id] = record # => co-33: stored for future outbound delivery
NEXT_ID[0] += 1 # => advances the counter for the NEXT subscription
return record # => echoes back what was just stored
SUBSCRIPTION_URL = "https://caller.example.com/hooks/articles" # => the caller's own delivery endpoint
SUBSCRIPTION_EVENTS = ["article.created", "article.deleted"] # => the two event types this caller wants
request = WebhookSubscriptionRequest(url=SUBSCRIPTION_URL, event_types=SUBSCRIPTION_EVENTS) # => co-33 request
stored = subscribe(request) # => co-33: registers the subscription
print(f"stored subscription: {stored}") # => Output: id, url, and both event types, echoed back
print(f"total subscriptions: {len(SUBSCRIPTIONS)}") # => Output: 1
# => the subscription is now stored server-side, ready for Example 55's signed deliveryRun: python3 example.py
Output:
stored subscription: {'id': 1, 'url': 'https://caller.example.com/hooks/articles', 'event_types': ['article.created', 'article.deleted']}
total subscriptions: 1Key takeaway: The WebhookSubscriptionRequest a caller submits and the stored record carry
the same information -- a webhook subscription is registered through an entirely ordinary POST,
with no special mechanism beyond what any other resource-creating endpoint already uses.
Why it matters: This inverts the usual request direction -- instead of the client polling the API for changes, the API itself becomes the caller of the CLIENT's own URL once a subscribed event occurs; Example 55 addresses the trust problem that inversion creates: how does the receiver know the delivery genuinely came from this API?
Example 55: Signing an Outbound Webhook Payload with HMAC
ex-55 · exercises co-33
Since a webhook receiver cannot verify TLS-level origin the way a browser does, the sender signs the payload with a SHARED SECRET (HMAC-SHA256); the receiver recomputes the same signature to confirm the payload is genuine.
learning/code/ex-55-webhook-hmac-sign/example.py
# pyright: strict
"""Example 55: Signing an Outbound Webhook Payload with HMAC. (co-33)
Since a webhook receiver cannot verify TLS-level origin the way a browser
does, the sender signs the payload with a SHARED SECRET (HMAC-SHA256); the
receiver recomputes the same signature to confirm the payload is genuine.
"""
import hashlib # => stdlib: the underlying hash function HMAC is built on
import hmac # => stdlib: computes and verifies the HMAC signature itself
SHARED_SECRET = b"webhook-shared-secret-key" # => co-33: known to BOTH sender and receiver, never sent
def sign_payload(payload: bytes, secret: bytes) -> str: # => co-33: the SENDER's own signing step
return hmac.new(secret, payload, hashlib.sha256).hexdigest() # => co-33: HMAC-SHA256 over the payload
def verify_signature(payload: bytes, signature: str, secret: bytes) -> bool: # => co-33: the RECEIVER's check
expected = sign_payload(payload, secret) # => co-33: recomputes the SAME signature independently
return hmac.compare_digest(expected, signature) # => co-33: constant-time comparison, timing-attack-safe
event_payload = b'{"event": "article.created", "id": 1}' # => the outbound webhook's own JSON body
signature = sign_payload(event_payload, SHARED_SECRET) # => co-33: the SENDER signs it before delivery
print(f"X-Webhook-Signature: {signature}") # => Output: a 64-char hex digest
genuine = verify_signature(event_payload, signature, SHARED_SECRET) # => the RECEIVER checks a genuine payload
print(f"genuine payload verifies: {genuine}") # => Output: True -- co-33: signatures match
tampered_payload = b'{"event": "article.created", "id": 999}' # => an ATTACKER-modified payload
tampered_check = verify_signature(tampered_payload, signature, SHARED_SECRET) # => co-33: checked against OLD sig
# => tampered_check is False -- the old signature no longer matches the new payload's own hash
print(f"tampered payload verifies: {tampered_check}") # => Output: False -- co-33: signature no longer matchesRun: python3 example.py
Output:
X-Webhook-Signature: 6efd92a41eedc43babee4325abebbafca75b9d8f8aa3cc3829362e944cd8119e
genuine payload verifies: True
tampered payload verifies: FalseKey takeaway: The tampered payload ("id": 999 instead of 1) still carries the ORIGINAL
signature computed over the genuine payload -- verify_signature recomputes the expected signature
from the payload it actually received, so any change to the bytes makes the two signatures diverge.
Why it matters: This closes the trust gap Example 54 opened -- the receiver of an inbound webhook call has no built-in way to confirm the call genuinely came from the API it subscribed to (as opposed to an attacker who guessed the receiver's URL); HMAC signing lets the receiver verify authenticity using only a secret shared once, at subscription time.
Example 56: A HAL Response with _links and _embedded
ex-56 · exercises co-28
HAL (application/hal+json) is a concrete hypermedia format: _links is REQUIRED (co-28's own
mechanism from Example 11, standardized), _embedded is optional -- inline sub-resources instead of
a second round trip.
learning/code/ex-56-hal-links/example.py
# pyright: strict
"""Example 56: A HAL Response with _links and _embedded. (co-28)
HAL (`application/hal+json`) is a concrete hypermedia format: `_links` is
REQUIRED (co-28's own mechanism from Example 11, standardized), `_embedded`
is optional -- inline sub-resources instead of a second round trip.
"""
from typing import Any # => a HAL document is arbitrary nested JSON
def hal_article(article_id: int, title: str, author_id: int, author_name: str) -> dict[str, Any]:
# => GET /articles/{id} -- co-28: builds a HAL-shaped response
article_links = { # => co-28: REQUIRED in HAL -- at minimum, a "self" link
"self": {"href": f"/articles/{article_id}"}, # => co-28: where THIS resource lives
"author": {"href": f"/authors/{author_id}"}, # => co-28: a link to a RELATED resource
} # => end of article_links
embedded_author = { # => the author's OWN mini-representation, embedded directly
"id": author_id, # => the author's own id
"name": author_name, # => the author's own name
"_links": {"self": {"href": f"/authors/{author_id}"}}, # => HAL nests recursively
} # => end of embedded_author
return { # => co-28: the standardized HAL envelope
"id": article_id, # => the resource's own plain attributes
"title": title, # => another plain attribute
"_links": article_links, # => co-28: REQUIRED -- at least a "self" link
"_embedded": {"author": embedded_author}, # => co-28: OPTIONAL -- inlines instead of a 2nd GET
} # => end of the hal_article construction
response = hal_article(1, "Hello, API Design", 7, "Ada") # => build one HAL response
print(f"_links: {response['_links']}") # => Output: self + author links
print(f"_embedded author: {response['_embedded']['author']}") # => Output: the inlined author record
# => a client can render the author WITHOUT a second GET /authors/7 round tripRun: python3 example.py
Output:
_links: {'self': {'href': '/articles/1'}, 'author': {'href': '/authors/7'}}
_embedded author: {'id': 7, 'name': 'Ada', '_links': {'self': {'href': '/authors/7'}}}Key takeaway: _links names the author's location (/authors/7) as a link, while _embedded
inlines the FULL author representation directly -- a client that only needs the author's id can
follow the link, and a client that needs the author's full details already has them without a
second GET.
Why it matters: HAL is a concrete, standardized answer to "what should HATEOAS actually look
like on the wire" (co-28) -- rather than every API inventing its own hypermedia convention, HAL
fixes the _links/_embedded shape so client libraries can be written once, against the format,
rather than once per API. A generic HAL client can therefore follow _links across completely unrelated APIs from different vendors, the same way a browser follows <a href> links across unrelated websites without custom code per site.
← Previous: Beginner Examples · Next: Advanced Examples →
Last updated July 28, 2026