FastAPI — Types Are the Contract
The Leaking Field and the Stalling Loop
Goal
You will cause on purpose and then fix the two things that blow up most often in real FastAPI work.
- A secret field leaking into a response (steps 3 to 4)
- Blocking inside
async defstopping the whole server (step 7)
Rules
- Create all files in
/root/work/api. - The app instance must be named
app, and the storage dependency must be namedget_store. The grader imports them by those names. - The grader imports your
app.pydirectly and sends requests to it. It grades even when uvicorn is not running (step 1 is the only exception: you must actually start it).
Starting the server
cd /root/work/api
uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 &
curl -s localhost:8000/healthz
Do not use --reload. After you edit a file, it is clearer what is running if you kill %1 and start it again.
Steps
/healthz→01-healthz.txt- Pydantic validation, 422 →
02-422.json - A deliberate leak →
03-leak.json - Block it with
response_model HTTPException404Depends(get_store)async defvsdef→07-block.txt·07-unblock.txt- A
dependency_overridestest - Wrap-up →
09-notes.md
Notes
Do not write validation code by hand. If you are writing if not isinstance(...),
you have taken over a job that type hints should do.
Start the server
Create a FastAPI app in /root/work/api/app.py and make GET /healthz return {"status":"ok"}. Actually start it with uvicorn, and save the result of curl to 01-healthz.txt.
mkdir -p /root/work/api && cd /root/work/api. app = FastAPI() must be named app; the grader imports it by that name. To start it: uvicorn app:app --host 0.0.0.0 --port 8000 > /tmp/uv.log 2>&1 &, then curl -s -i localhost:8000/healthz > 01-healthz.txt.
Make invalid input get rejected
Create POST /items and accept the body as a Pydantic model. The model must have name: str and qty: int. Send a string for qty, receive 422, and save that body to 02-422.json.
class ItemIn(BaseModel): name: str; qty: int and def create(item: ItemIn). Do not write a single line of validation code; the type is the validation. curl -s -X POST localhost:8000/items -H 'content-type: application/json' -d '{"name":"a","qty":"many"}' > 02-422.json.
Leak a secret field on purpose
Create GET /me and have it return an object that has both email and hashed_password, as it is. Save the response, in which the hash comes out as it is, to 03-leak.json. In this step the right answer is the leak.
Without response_model, if you return a dict or a model as it is, everything goes out. This is an accident that really happens often, and the next step blocks it.
Block it with an output model
Add GET /me/safe and use response_model so that hashed_password disappears from the response. The handler may still return the whole object.
The output-only model (UserOut) holds only email. @app.get("/me/safe", response_model=UserOut). The point is that the field gets filtered out without touching the handler code. Never use the same class for the input model and the output model.
Return 404 for things that do not exist
Create GET /items/{item_id} and, for an id that does not exist, return 404 with a human-readable message.
raise HTTPException(status_code=404, detail="..."). You must not return 200 with return {"error": ...}; the status code is part of the contract.
Inject the store as a dependency
Change the code so that the shared store is injected with Depends. Name the function that creates the store get_store.
Create def get_store(): ... and receive it in the handler as store = Depends(get_store). Do not reference a global variable directly; in a later step you will swap the whole thing out.
Stop the event loop, then fix it
Create two routes that both do time.sleep(0.5). GET /slow is async def and GET /slow2 is def. Fire 4 requests at each at the same time and save the elapsed times to 07-block.txt and 07-unblock.txt. The first must take 2 seconds or more, and the second must finish within 1 second.
To measure: time (for i in 1 2 3 4; do curl -s localhost:8000/slow & done; wait) 2>&1 | tee 07-block.txt. The code is identical except for one word (async), yet the time differs by a factor of 4. The first lines up on a single event loop, and the second is sent to the thread pool by FastAPI. The conclusion of this lab is that when you are not sure, it is safer to write def.
Swap the dependency and test
Write test_app.py and include at least one test that uses dependency_overrides to replace get_store with a fake. pytest -q must pass.
from fastapi.testclient import TestClient, app.dependency_overrides[get_store] = lambda: {...}. The real value of Depends is that you can swap it without changing a single line of production code. Do not use monkey patching.
Summarize the two accidents
Write three lines in 09-notes.md: (1) what leaked in step 3, (2) why the time differed by a factor of 4 in step 7, and (3) what blocked each of them.
The two words response_model and def must appear in the text. These two are the accidents that blow up most often in FastAPI.