Day 41 ยท Ordering at the counter

HTTP & Build Your First API

You will be able to
  • Dissect an HTTP request and response: method, path, headers, status, body
  • Choose correct methods and status codes for CRUD-style operations
  • Use httpx to call APIs with params, headers, timeouts, and error handling
  • Build a FastAPI service with path/query parameters and pydantic validation
  • Explore your own API through its auto-generated docs
Today's ~120 minutes
Spaced-rep warm-up: due cards (async, GIL, semaphores)10 min
ELI5 + tech read; trace one request through the http-cycle visualizer18 min
Guided: build the counter, then be the customer42 min
Practice: the resilient client20 min
Project: quotes API + client20 min
Quiz + flashcards10 min

Builds on: Day 40 โ€” Concurrency & async ยท Day 10 โ€” OOP II โ€” dataclasses (pydantic's shape) ยท Day 5 โ€” JSON handling

The analogy

A coffee counter has a protocol everyone silently knows. You state a VERB and a THING: "GET me a latte." The barista replies with a STATUS before anything else โ€” "coming up" (all good), "we don't have that" (your mistake), "the machine is broken" (their mistake) โ€” and then, maybe, the drink. You can also hand things over: "POST: here's my custom order form." Neither of you remembers the last exchange; every order is complete in itself, which is why you repeat "for Priya, oat milk" every single time.

HTTP is exactly this, written down. The request line says the verb and the thing (GET /books/7); headers are the sticky notes on the order ("I speak JSON", "I'm Priya"); the body is the form you handed over. The response leads with a three-digit status โ€” 2xx coming-up, 4xx your-mistake, 5xx our-mistake โ€” then its own headers and body. Statelessness is the counter's amnesia: every request must carry everything needed to serve it.

Today you stand on BOTH sides of the counter: morning as the customer (httpx, the caller), afternoon as the barista (FastAPI, the server) โ€” and the customer half is how you will talk to every LLM API for the rest of the program.

Why this matters on the job

HTTP is the substrate of your entire future stack: every LLM call (Day 106) is an HTTP POST, every 429 you retry (Day 107), every webhook, every RAG service endpoint. Building the server side is just as core: the AI features you ship are consumed as APIs, and FastAPI is the Python default โ€” async-native (yesterday's loop, now serving requests), pydantic-validated (the same contract thinking as Day 36's schemas, at the door of your service), with free interactive docs your customers and teammates will actually use. The Day 42 checkpoint, the Day 49 URL shortener, and the capstone's serving layer are all today's skeleton, thickened.

Watch it happen

Ordering at the counter โ€” one HTTP request, end to end

step 1 / 6
Browserclientโ†’DNSphonebookโ†’Serverapi.shop.comโ†’Responsestatus + body
โœ‰ GET https://api.shop.com/orders

You ask for a URL. Before anything can be ordered, the browser needs the server's street address โ€” domain names are for humans, not routers.

Guided practice

guided 1

Stand up the counter (FastAPI)

22 min
  1. Install the day's tools in your venv: pip install fastapi uvicorn httpx. Create api.py from the starter โ€” an in-memory books API (dict for storage; real persistence arrives tomorrow).
  2. Run uvicorn api:app --reload and open http://127.0.0.1:8000/docs. Click through the auto-docs: expand GET /books, try it, read the curl it generated. This page came entirely from your type hints.
  3. Poke the validation without writing any client code: try GET /books/abc (422 โ€” path type failed), POST a book with a negative price (422 with a field-level error message locating exactly what and where), then a valid POST (201, echoing the created resource with its id).
  4. Read the starter's status-code choices out loud and justify each: 201 for create, 404 for missing id, 422 arriving free from pydantic. Then add a DELETE route yourself returning 204 โ€” and confirm in /docs that it appeared.
  5. Watch the uvicorn console as you click: every line is method, path, status โ€” the request log you will grep in production (Day 16 pays off here).
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Be the customer (httpx)

20 min
  1. With the server still running, create client.py. Use one httpx.Client(base_url=..., timeout=5.0) for all calls (connection reuse โ€” and a timeout, always).
  2. POST three books, then GET the list with params={"author": ..., "limit": 2} โ€” print the URL httpx actually built (response.request.url) to see query-string encoding done for you.
  3. Handle failure tiers properly: GET /books/999 and branch on status_code โ€” 404 is not an exception at transport level, and blind .json()["title"] on it would KeyError. Then use raise_for_status() in a try/except as the alternative style; print status_code and the error body from the exception.
  4. Inspect a full exchange: print request method/URL/headers and response status/headers (find content-type and content-length). Match each piece to the request-anatomy diagram from the tech section.
  5. Finish with the async version: rewrite the three POSTs with httpx.AsyncClient and asyncio.gather โ€” yesterday's pattern, today against a real server. Time sequential vs gathered.
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

The resilient client

20 min

Make your API flaky, then build the client that survives it. Add GET /flaky to api.py: 30% of calls return 503, 20% return 429 with a Retry-After: 1 header, the rest 200. Then write resilient.py: a get_with_retries(client, url, max_tries=4) function implementing exponential backoff with jitter (0.5s base, doubling, plus random 0โ€“0.3s), honoring Retry-After when present, retrying ONLY retryable statuses (429, 502, 503 โ€” never 404), and logging each attempt with its wait. Prove it: 50 calls through the wrapper should all eventually succeed; print the attempt histogram.

Constraints: timeouts on every call; a final failure after max_tries raises a clear exception. Think: why is retrying a GET always safe, and when would retrying a POST double-charge someone?

Hints: wait = base * 2**attempt + random.uniform(0, 0.3); Retry-After arrives as seconds in r.headers.

Ship before you stop

Quotes API โ€” designed, built, documented

Design and build quotes_api.py solo: a quotes service with POST /quotes (text, author, tags list โ€” validated: non-empty text, at most 5 tags), GET /quotes (filterable by author and tag via query params, with limit/offset pagination), GET /quotes/{id}, DELETE /quotes/{id}, and GET /quotes/random. Correct status codes throughout (201/204/404/422). Then ship quotes_client.py exercising every route including the failure paths, using one Client with timeout. Screenshot or describe your /docs page in the README section. In-memory storage is fine โ€” tomorrow it grows a real database and tests. Commit both files.

Rubric โ€” check what you completed (0/5)

Common mistakes & misconceptions

  • No timeout on HTTP calls. The default is to wait forever; one hung upstream hangs your service. timeout= on every client โ€” non-negotiable from today.
  • Assuming a 2xx because no exception was raised. httpx returns 404s and 500s as normal responses โ€” check status_code or call raise_for_status().
  • Using GET for state-changing actions ("GET /books/7/delete"). Crawlers, prefetchers, and retries will happily fire it repeatedly โ€” verbs carry contracts.
  • Returning 200 with {"error": "not found"} in the body. Clients branch on status codes; lying with 200 breaks every generic client, cache, and monitor downstream.
  • Blocking inside an async def handler (sync DB call, time.sleep) โ€” yesterday's cardinal sin, now freezing every concurrent request your server is juggling.
  • Retrying non-idempotent POSTs blindly on timeout โ€” the request may have succeeded before the timeout hit: double-charge. Retry idempotent calls freely; POSTs need idempotency keys (Day 107).
Knowledge check

Q1. A client sends valid JSON to your FastAPI route, but price is -5 against Field(ge=0). What does FastAPI return?

Q2. Which request is safe to retry automatically after a timeout, and why?

Q3. Your service calls a dependency that starts returning 503. The well-behaved client response isโ€ฆ

Go deeper โ€” curated resources

docsMDN โ€” An overview of HTTP โ†—20 mindocsFastAPI โ€” official tutorial (first steps through body validation) โ†—30 mindocshttpx โ€” official documentation (quickstart) โ†—15 min
If you have a third hour
  • FastAPI โ€” async def vs def routes โ†— โ€” Read how FastAPI runs plain-def handlers in a threadpool but async handlers on the loop โ€” then connect it to yesterday's blocking-call sin and decide which your DB calls need tomorrow.
Done means
  • Books API serving with /docs explored; DELETE route added and verified
  • Client handles 404 and validation-failure branches without crashing
  • Resilient wrapper survives 50 flaky calls with logged backoff
  • Quotes API + client committed with correct status codes throughout
  • Quiz โ‰ฅ 2/3
How this connects

โ† Back: Handlers run on Day 40's event loop (its cardinal sin now a serving outage); pydantic models are Day 10's dataclasses with Day 36's contract enforcement; the JSON bodies are Day 5's serialization on the wire.

Forward โ†’: Tomorrow this API gains a real database and TestClient tests. Day 43 opens the network layers beneath it, Day 44 adds auth, Day 45 hardens the architecture โ€” and Day 106's LLM calls are exactly today's httpx POSTs with an Authorization header.

Unlocks: D42 Week 6 Checkpoint: API + DB Mini-Service ยท D43 Networking โ€” Packets to HTTPS ยท D44 Security, AuthN & AuthZ ยท D68 Cleaning & Validation