How the Web Works & APIs
HTTP end-to-end, and testing APIs like a pro
HTTP is a conversation with strict etiquette. Your browser (or curl, or your code) sends a REQUEST — method, path, headers, optional body — and the server answers with a RESPONSE — status code, headers, body. That's it. Every API, every website, every agent tool call is this same conversation.
Once you can read the two sides of the conversation, 'the API doesn't work' becomes 'the API returned 422 because I sent a string where it wanted a number'.
Reading a real response, headers and all
`curl -i` shows you the whole conversation. This is a real capture against a public API — note the status line, the caching header, and the rate-limit headers that tell you how polite you must be:
curl -i https://jsonplaceholder.typicode.com/todos/1
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: max-age=43200
etag: W/"53-hfEnumeNh6YirfjyjaujcOPPT+s"
x-ratelimit-limit: 1000
x-ratelimit-remaining: 999
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}Status codes are the headline: 2xx = it worked, 3xx = go look over there, 4xx = you (the client) did something wrong, 5xx = the server broke. The ones you'll see daily: 200, 201, 204, 301, 400, 401 (not logged in), 403 (not allowed), 404, 409, 422 (validation), 429 (slow down), 500, 502, 503, 504. When nginx sits in front of your app, 502 means 'the app behind me died'.
JWT: header.payload.signature — and why it's not secret
A JWT is three base64-encoded JSON parts joined by dots. Anyone can DECODE it — it is not encrypted. The signature is what matters: the server recomputes HMAC over the header and payload with its secret, and if it doesn't match, the token is rejected. That's how a token can be 'readable by anyone but forgeable by no one'.
import base64, json
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzg1NTk2ODg0fQ.signature"
def b64decode(part: str) -> dict:
pad = part + "=" * (-len(part) % 4) # base64url needs padding
return json.loads(base64.urlsafe_b64decode(pad))
print(b64decode(token.split(".")[0])) # {"alg": "HS256", "typ": "JWT"}
print(b64decode(token.split(".")[1])) # {"sub": "user_42", "role": "admin",
# "exp": 1785596884}
# NEVER put passwords in a JWT payload. It's base64, not encryption.Test APIs like a professional: hermetic, repeatable, in CI
Manual testing with curl is for exploring. Automated testing is for staying sane when an AI changes your API. The pattern: mock every external API at the transport level so tests never touch the network — fast, deterministic, and runnable in CI:
import httpx, respx
@respx.mock
def test_fetch_user_never_hits_the_network():
# register a fake response for the external API
respx.get("https://api.example.com/users/42").mock(
return_value=httpx.Response(200, json={"name": "Ada"})
)
r = httpx.get("https://api.example.com/users/42")
assert r.status_code == 200
assert r.json()["name"] == "Ada"
assert respx.calls.call_count == 1 # exactly one request, mockedAnd when you hand an API to an agent: FastAPI generates OpenAPI docs at /docs automatically. An agent that reads the spec writes correct calls on the first try — that's the real 'brief the agent' move.
test yourself
01A 422 from an API means:
02A JWT is:
03Why does your browser get a CORS error while curl works fine?
the real check
bru run passes in CI. A mocked test proves no live network in your test suite.
0 stamps earned·streak: 0 days
Signed out: progress lives in this browser. to carry it everywhere.
vibe-coder payoff
80% of vibe coding is wiring APIs together. This removes the magic.