Don't read this before submitting your attempts. The mistakes you don't make are the ones you don't learn from.
Exercise 1 — Anatomy with curl -v
- Request line:
GET /api/health/ HTTP/1.1; status line:HTTP/1.1 200 OK. Three headers (any of):Date,Server,Content-Type,Content-Length,Vary,Allow... Body:{"status":"ok","service":"ticketflow"}. - With
Accept: text/html, DRF negotiates content: since the endpoint usesapi_viewand the HTML renderer is enabled by default in DEBUG, it would return DRF's browsable HTML (or a 406 if no compatible renderer). TheAcceptheader guides which representation of the same resource you want.
Exercise 2 — Contract design
Request (happy path):
POST /api/events/42/reserve HTTP/1.1
Host: ticketflow.example.com
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7f3c9a2e ← anti-duplicate key in case the client retries
{"seats": [1051, 1052]}Happy response:
HTTP/1.1 201 Created
Location: /api/reservations/9172/
Content-Type: application/json
{"id": 9172, "status": "pending_payment", "seats": [1051, 1052], "expires_at": "..."}Conflict response:
HTTP/1.1 409 Conflict
{"code": "seat_already_reserved", "detail": "Seat 1051 is already reserved"}Comment: 409 Conflict is the code for state clashes (seat taken). 422 is also defensible, but 409 communicates better "your request is valid, but the world changed". We'll dig into this in Lesson 10 (transactions: the seat is decided inside a transaction with a lock, not by "check and reserve").
Exercise 3 — Fix the code
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
@api_view(["POST", "DELETE"]) # if used as DELETE-style cancellation, DELETE is idempotent
def cancel_reservation(request, pk):
try:
reservation = Reservation.objects.get(pk=pk)
except Reservation.DoesNotExist:
return Response({"detail": "Not found"}, status=status.HTTP_404_NOT_FOUND)
reservation.status = "cancelled"
reservation.save()
return Response({"status": "cancelled"}, status=status.HTTP_200_OK)Original mistakes:
- Implicit 200: without
status=, DRF answers 200. If the endpoint updates state, 200 is acceptable; but if the design were "DELETE the reservation", the right answer is 204 No Content. The real mistake is ambiguity: never leave the status code to chance. - 500 when it doesn't exist:
objects.getraisesDoesNotExist→ Django turns it into a 500 in an API view unless you catch it or useget_object_or_404. A missing resource is 404, not 500.
Senior bonus: cancelling should be idempotent: cancelling the same reservation twice shouldn't fail (return 200 with cancelled state or 204). We'll return to this in Lesson 14.
Exercise 4 — CORS in practice
- With
Origin: http://localhost:5173: a 200 OK (preflight) response with headers likeAccess-Control-Allow-Origin: http://localhost:5173,Access-Control-Allow-Methods:...POST...,Access-Control-Allow-Headers: authorization, content-type. The browser considers the request authorized. - With
Origin: http://evil.example: django-cors-headers doesn't emit theAccess-Control-*headers (the request may still answer 200 but without CORS authorization). The browser will block it. Fine note: the block is done by the browser, not the server;curlnever blocks. - After adding
Idempotency-KeytoCORS_ALLOW_HEADERS, the preflight response includes it inAccess-Control-Allow-Headers. Without that, the browser would reject any real request that sends it.
What you just tested: the exact mechanism a frontend on localhost:5173 uses to consume your API on localhost:8000.
Exercise 5 — Secure cookies
Set-Cookie: sessionid=...; Path=/; SameSite=Lax(plusHttpOnlyfor Django's session cookie; via the admin you'll also seecsrftoken).- With
SESSION_COOKIE_SECURE = True, the cookie does not travel over plain HTTP:curl http://...won't see it in the response (and a browser would discard it). It's only sent over HTTPS. - -
HttpOnly: JS can't read it → XSS can't steal the session. Secure: never sent in the clear → MitM can't capture it.SameSite=Lax/Strict: the browser won't attach it on cross-site requests → mitigates CSRF.
Exercise 6 — ETag and 304
- First response: 200 with
ETag: "<hash>"andCache-Control: max-age=60,...(plusExpires,Last-Modifieddepending on the helper). - With
If-None-Match: "<your-etag>": a 304 Not Modified response and an empty body. - Savings: the server neither serializes nor transfers the body (and the client doesn't download it); only headers are exchanged. On large listings that's a huge win. Trade-off: validation implies comparing ETags (trivial here; with dynamic content you must compute them well, or you'll regenerate everything anyway).
Exercise 7 — Reason like a designer
- PATCH
/reservations/1051with{"expires_at": "+10min"}(or a subresource/reservations/1051/extension) → 200. It's a partial modification of the resource, not a creation. - PATCH
/events/42with{"status": "cancelled"}→ 200 (or PUT if you replace the whole representation; PUT would be idempotent). Marking as cancelled deletes nothing: it is not DELETE. - GET
/events/42/availability→ 200 (cacheable with a shortCache-Controlor an ETag; availability changes, so use a low TTL). - POST
/checkout→ 202 Accepted with a body like{"job_id": "...", "status": "queued"}. The process is queued (Lesson 06) and confirmation arrives via polling or webhook. 202 communicates exactly "accepted, not ready yet".
Exercise 8 — HTTPS and TLS
- Confidentiality (defeats a network eavesdropper: open Wi-Fi, ISP), integrity (defeats whoever alters packets / a malicious proxy) and server authenticity (defeats phishing/MITM with a fake certificate: the CA is who tells you the server is who it claims to be).
- TLS 1.3 reduces the handshake to 1 round-trip (and 0 with session resumption/TLS 1.3 over an established connection or QUIC): fewer RTTs before the first request = lower TTFB on every new connection. It also removes legacy ciphers.
- TLS terminates at Nginx: the client encrypts up to Nginx; from Nginx to Gunicorn/Django traffic usually travels in the clear inside the private network. That's "TLS termination". Implication: internal traffic must be protected by other means (private network, mTLS or internal TLS) — an architecture detail we'll revisit in Lesson 02 and Module 9.
Professor's summary
- HTTP is simple: method + path + headers + body; the hard part is semantic discipline (correct codes, idempotency, header security).
- A good backend doesn't just answer JSON: it answers with the right status code, the right headers and consistent bodies.
- Everything from today is verified with
curl -i/-v. Always do it: headers lie less than documentation.