Stack: Django + DRF · Project: TicketFlow Status: Taught — exercises pending submission Prerequisite: Lesson 00 — Environment and your first endpoint
Objectives
By the end of this lesson you will be able to:
- Describe the exact anatomy of an HTTP request and response.
- Choose the right method and status code with justification (interviewers this).
- Explain which headers control CORS, caching, cookies and compression, and verify them with
curl. - Reason about keep-alive, HTTPS/TLS and why they matter in production.
- Apply all of this in DRF: why
Responsepicks status codes for you, and when not to let it.
1. Anatomy of an HTTP request
HTTP is a text protocol (binary since HTTP/2) over TCP, stateless: every request carries all the information needed. Structure:
POST /api/events/42/reserve HTTP/1.1 ← method + path + version
Host: ticketflow.example.com ← mandatory in HTTP/1.1
Content-Type: application/json ← what I'm sending
Authorization: Bearer eyJhbGci... ← who I am
Idempotency-Key: 7f3c9a2e ← anti-duplicates (Lesson 14)
Content-Length: 58
{"seat_id": 1051, "quantity": 2} ← body (optional)And the response:
HTTP/1.1 201 Created
Location: /api/reservations/9172/ ← where the created thing lives
Content-Type: application/json
Cache-Control: no-store
{"id": 9172, "status": "pending_payment", "seats": [1051, 1052]}Key interview concept: HTTP is stateless but can keep a session via cookies or tokens. Don't confuse protocol state with application state.
Methods: safe, idempotent, cacheable
| Method | Safe | Idempotent | Typical use in TicketFlow |
|---|---|---|---|
| GET | list events, view availability | ||
| POST | create reservation, start checkout | ||
| PUT | replace a whole event | ||
| PATCH | * | edit an event's price | |
| DELETE | cancel a reservation |
\* PATCH is idempotent only if the patch is absolute ({"price": 20}), not relative ({"price": "+2"}).
- Safe = doesn't modify server state (the client can repeat it fearlessly).
- Idempotent = repeating N times yields the same result as once. Crucial for payments and webhooks (Lesson 14).
Status codes: the ones you'll actually use
| Code | Meaning | TicketFlow example |
|---|---|---|
| 200 OK | generic success | GET availability |
| 201 Created | resource created | POST reservation (with Location) |
| 204 No Content | success without body | DELETE reservation |
| 400 Bad Request | malformed / invalid request | negative seat count |
| 401 Unauthorized | not authenticated (the name misleads) | no token |
| 403 Forbidden | authenticated but not allowed | buyer tries to edit an event |
| 404 Not Found | doesn't exist (or you don't want to admit it exists) | — |
| 409 Conflict | state clash | seat already reserved |
| 422 Unprocessable | well-formed but business-invalid | event already past |
| 429 Too Many Requests | rate limit | a bot buying everything |
| 500 | our bug | our failure, not the client's! |
Golden rule: 4xx are the client's fault; 5xx, the server's. Returning 500 for a failed validation is a bug in your API.
2. Headers you master or suffer
CORS (Cross-Origin Resource Sharing)
The browser blocks responses from another origin by default. CORS is how the server authorizes those origins. A preflight OPTIONS request with:
Access-Control-Allow-Origin: https://ticketflow.app
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type, Idempotency-KeyIn DRF, django-cors-headers manages it (we install it in the exercises). CORS is enforced by the server; it is not a client-side security measure: if the public API has no CORS, it's because it doesn't want browsers from other origins; curl always ignores CORS.
Cookies and security attributes
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/HttpOnly→ JavaScript can't read it (mitigates XSS stealing the session).Secure→ only travels over HTTPS.SameSite=Lax|Strict|None→ mitigates CSRF by restricting cross-site submissions.- Expiry:
Expires/Max-Age; without them it's a session cookie (dies when the browser closes).
HTTP caching
Cache-Control: max-age=60, public # strong policy (recommended)
ETag: "v17" # identifier of the resource version- The client repeats the request with
If-None-Match: "v17"→ if unchanged, the server answers 304 Not Modified with no body: bandwidth saved. Cache-Control: no-storefor sensitive data (payments).no-cache= "you may cache but always revalidate" (a misleading name).- Shared cache (CDN/proxy) vs private:
privatekeeps a CDN from caching responses containing user data.
Negotiation and body
Content-Type: application/json(request) and response. DRF does content negotiation: withAccept: application/jsonvstext/html(the browser shows the browsable HTML of the APIView).Accept-Language,Accept-Encoding: gzip, br→ response compression (done by the server or the proxy, Lesson 02).
3. Keep-alive, HTTPS and TLS
Keep-alive and the cost of connections
HTTP/1.0 opened a TCP connection per request (expensive: TCP handshake + TLS). HTTP/1.1 introduced persistent connections (Connection: keep-alive by default): many requests reuse the connection. HTTP/2 goes further with multiplexing (several requests in parallel over one connection).
In production your Django doesn't talk to the world directly: Gunicorn/Uvicorn behind Nginx keeps keep-alive towards the client and efficient connections towards the app (Lesson 02).
HTTPS/TLS in 4 steps
- Handshake: client and server negotiate version and ciphers.
- Certificate: the server presents its certificate signed by a CA the client trusts.
- Keys: (classically) via Diffie-Hellman exchange, a symmetric session key is derived.
- Symmetric encryption for all data (fast).
This gives confidentiality (nobody reads), integrity (nobody alters) and authenticity (the server is who it claims to be). In 2026 the standard is TLS 1.3: 1 round-trip (0 on resumption) and modern ciphers.
HSTS: the
Strict-Transport-Securityheader tells the browser "only speak HTTPS to me for X time", preventing downgrade attacks.
4. HTTP in DRF: what the framework does for you (and shouldn't)
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["POST"])
def reserve(request):
if request.method == "POST":
# DRF: request.data already parses JSON according to Content-Type
# Response without status= would return 200: to create a resource, say 201!
return Response({"status": "pending_payment"}, status=status.HTTP_201_CREATED)Points a senior controls:
- Explicit codes:
Response's silent default is 200. For aPOSTthat creates → 201; for business actions → 200; for "accepted for queue processing" → 202 (Lesson 06). request.datavsrequest.query_params: body vs query string. Confusing them is a bug factory.- Consistent errors: DRF returns
{"detail":...}or{"field": [errors]}; we'll define our own centralized error format (Lesson 26). - Response headers:
Response(headers={"Location":...}),Cache-Controlon cacheable listings. - 401 vs 403 in DRF:
NotAuthenticated(401) vsPermissionDenied(403). DRF resolves it, but you must be able to explain it.
Self-assessment (answer me in the chat)
- Why is
POST /reservenot idempotent, and what are the consequences if the client retries after a timeout? - The client sends valid credentials but their role doesn't allow that action. 401 or 403? Why?
- Which header(s) would you use so a public events listing is cached 60s on a CDN but payment data never is?
- What exactly happens when the browser does a preflight and the server doesn't allow the
Idempotency-Keyheader? - Why is returning 500 for a malformed JSON a backend error, not a client error?
Continue with the exercises. The solutions only after trying it yourself.