C.W.K.
Stream
Lesson 03 of 04 · published

Build Your Own CRUD — The End-to-End That Cements Everything

~12 min · epilogue, crud, synthesis, capstone

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Reading about REST is not the same as building one. The capstone exercise: build a complete CRUD service that exercises every track from this quest. Once you've done it once, the patterns become reflex."

The Spec — One Resource, Eight Endpoints, Every Concept

Build a tiny /items service that exercises everything:

  • GET /items — list with cursor pagination + sorting + sparse fieldsets + filter.
  • POST /items — create with server-assigned ID, return 201 + Location.
  • GET /items/{id} — read with ETag + Cache-Control + 304 conditional GET.
  • PUT /items/{id} — full replace with If-Match for optimistic locking (412 on conflict).
  • PATCH /items/{id} — partial update.
  • DELETE /items/{id} — remove, return 204.
  • POST /items/{id}/publish — action endpoint (no natural noun for 'publish').
  • POST /items/jobs — async bulk operation, return 202 + Location to a job-status resource.

Plus cross-cutting: CORS, request_id middleware, structured error envelopes, OpenAPI at /docs, Sunset header on a deprecated endpoint, Bearer token auth, rate limit.

The Build Order

  1. FastAPI app + Pydantic Item model + in-memory dict store. GET /items returns the list.
  2. Add POST /items with 201 + Location and server-generated itm_-prefixed IDs.
  3. Add GET /items/{id}. Then add ETag computation (sha256 of sorted JSON) and 304 on If-None-Match match.
  4. Add PUT /items/{id} with If-Match (412 if stale ETag), PATCH for partial, DELETE with 204.
  5. Add cursor pagination (limit + cursor params, fetch limit+1 to detect has_more), filter (whitelist), sort (whitelist), fields (whitelist).
  6. Add action endpoint POST /items/{id}/publish with 202 if async.
  7. Add async job pattern: POST /items/jobs returns 202 + Location, GET /items/jobs/{job_id} reports status.
  8. Add CORS middleware, correlation-id middleware, custom error envelope.
  9. Add Bearer auth via FastAPI HTTPBearer dependency, with WWW-Authenticate on 401.
  10. Add slowapi rate limit (10/min per IP). Add Sunset header on a deprecated /v1/items route.

By the end, you've shipped a textbook-clean REST service in 200-300 lines of Python. Every track of this quest is exercised.

The Quality Bar

The endpoints work; now check the polish:

  • Hit each endpoint with curl -v; verify status codes match the spec.
  • Send the same POST twice with an Idempotency-Key; verify only one item is created.
  • Issue a conditional GET with a stale ETag vs a current one; verify 200 vs 304.
  • PUT with a stale If-Match; verify 412.
  • Send a malformed body; verify the error envelope has code, message, request_id.
  • Visit /docs; verify Swagger UI shows every endpoint with its types.
  • Hit the rate limit; verify Retry-After header present.
  • Hit the deprecated route; verify Sunset header.

Every "verify" is a moment of "yes, the protocol does what I think it does." That's the muscle memory you're building.

The capstone isn't a project; it's a reflex builder. Once you've wired up Location, ETag, If-Match, 412, correlation IDs, error envelopes, OpenAPI, and Sunset headers in one service, all of it becomes obvious. The next time you build a REST API, you're not reaching for tutorials — you're shaping the bytes deliberately.

Extensions for the Bold

Optional ambition beyond the basic spec:

  • Add JWT instead of opaque tokens. Verify with PyJWT; pin algorithm; include claims (sub, exp).
  • Add a SSE endpoint for streaming creates as they happen.
  • Add a WebSocket endpoint for bidirectional collaborative editing.
  • Add Redis as the dedupe store for Idempotency-Key instead of in-memory.
  • Add OpenTelemetry traceparent propagation for distributed tracing.
  • Generate a TypeScript SDK from the OpenAPI spec; use it from a tiny React frontend.

Each extension exercises a track in more depth. None are required for the capstone; all are well-trodden territory.

cwkPippa as the Cheat Sheet

If you get stuck building any part, cwkPippa's source is a worked example. Stuck on FastAPI middleware ordering? See backend/main.py. Stuck on streaming responses? See backend/adapters/claude.py. Stuck on healing / idempotency? See backend/store/conversations.py. Reading working code beats reading tutorials when you have a specific question. cwkPippa is yours to crib from.

Code

Capstone scaffold — auth, CORS, ETag computation, 201 + Location·python
# The capstone scaffold — ~80 lines for the bones
import hashlib, json, secrets, time, uuid
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Response, Header, status, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field

app = FastAPI(title='Items API', version='1.0.0')
app.add_middleware(
    CORSMiddleware,
    allow_origins=['http://localhost:5173'],
    allow_credentials=True, allow_methods=['*'], allow_headers=['*'],
)
bearer = HTTPBearer()
TOKENS = {'tok_valid_xyz': 'u_42'}

class Item(BaseModel):
    id: str = Field(..., pattern='^itm_')
    name: str
    description: str | None = None
    version: int

_ITEMS: dict[str, dict] = {}
_JOBS: dict[str, dict] = {}

def compute_etag(d: dict) -> str:
    body = json.dumps(d, sort_keys=True, separators=(',', ':')).encode()
    return f'"{hashlib.sha256(body).hexdigest()[:12]}"'

async def current_user(creds: HTTPAuthorizationCredentials = Depends(bearer)):
    uid = TOKENS.get(creds.credentials)
    if not uid:
        raise HTTPException(401, detail='invalid token',
                            headers={'WWW-Authenticate': 'Bearer'})
    return uid

@app.post('/items', status_code=201)
async def create_item(payload: dict, response: Response, uid=Depends(current_user)):
    item_id = 'itm_' + secrets.token_urlsafe(8)
    item = {'id': item_id, 'version': 1, **payload}
    _ITEMS[item_id] = item
    response.headers['Location'] = f'/items/{item_id}'
    response.headers['ETag']     = compute_etag(item)
    return item

# ... continue with GET / PUT / PATCH / DELETE / cursor pagination / action / async
#     (see the exercise for the full build out)
Conditional GET (304) + optimistic update (412) wired in·python
# The optimistic update + conditional GET combo (the harder half)
from fastapi import Header

@app.get('/items/{item_id}')
async def read_item(item_id: str, response: Response,
                    if_none_match: str | None = Header(None, alias='If-None-Match'),
                    uid=Depends(current_user)):
    item = _ITEMS.get(item_id)
    if not item:
        raise HTTPException(404)
    etag = compute_etag(item)
    response.headers['ETag'] = etag
    response.headers['Cache-Control'] = 'private, max-age=60, must-revalidate'
    response.headers['Vary'] = 'Authorization'
    if if_none_match == etag:
        response.status_code = 304
        return None
    return item

@app.put('/items/{item_id}')
async def replace_item(item_id: str, payload: dict, response: Response,
                       if_match: str | None = Header(None, alias='If-Match'),
                       uid=Depends(current_user)):
    existing = _ITEMS.get(item_id)
    if not existing:
        raise HTTPException(404)
    current_etag = compute_etag(existing)
    if if_match and if_match != current_etag:
        raise HTTPException(412, detail='ETag mismatch; refetch and retry')
    new = {**existing, **payload, 'version': existing['version'] + 1}
    _ITEMS[item_id] = new
    response.headers['ETag'] = compute_etag(new)
    return new
Verify each requirement — curl-based smoke test·bash
# Verify your capstone against every requirement

# Create returns 201 + Location
curl -i -X POST http://localhost:8000/items -H 'Authorization: Bearer tok_valid_xyz' \
  -H 'Content-Type: application/json' -d '{"name":"first"}'

# Save the ETag from the response
ETAG=$(curl -s -i http://localhost:8000/items/itm_xyz -H 'Authorization: Bearer tok_valid_xyz' | grep -i etag | awk '{print $2}' | tr -d '\r')

# Conditional GET — should be 304 with current ETag
curl -i http://localhost:8000/items/itm_xyz -H 'Authorization: Bearer tok_valid_xyz' -H "If-None-Match: $ETAG"

# Optimistic update — should be 412 with stale ETag
curl -i -X PUT http://localhost:8000/items/itm_xyz -H 'Authorization: Bearer tok_valid_xyz' \
  -H 'Content-Type: application/json' -H 'If-Match: "v99-stale"' -d '{"name":"updated"}'

# OpenAPI / Swagger UI available
open http://localhost:8000/docs

External links

Exercise

Build the capstone /items service in FastAPI. Hit every endpoint with curl -v; verify each status code, each header (ETag, Location, Cache-Control, Vary, Retry-After, Sunset, X-Request-ID, WWW-Authenticate). Bonus: write a tiny test suite that exercises each endpoint and asserts on response shapes — this becomes the regression net for any future changes. Triple bonus: generate a TypeScript SDK from the OpenAPI spec and write a tiny client script that uses it end-to-end.
Hint
Start with the FastAPI scaffold from the code blocks; extend gradually (don't try to write everything at once). Verify each piece with curl before adding the next. The test suite teaches you that the spec wasn't just documentation — it was an assertion you can re-run anytime. The TypeScript SDK demo closes the loop: one OpenAPI spec → typed client → working app.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.