The Same API, in FastAPI
Apologetic question: "What is a promise made in precise words?"
Project 10 — The Same API, in FastAPI
“Let what you say be simply ‘Yes’ or ‘No’; anything more than this comes from evil.” — Matthew 5:37 (ESV)
Chapter: 10 — APIs, JSON, and FastAPI
Due: End of Week 10
Submit: A link to a public GitHub repo containing main.py, models.py, requirements.txt, README.txt, and agent-log.txt. Run it with a real local toolchain + git — see Appendix A.
Allowed tools: Python 3.11+, FastAPI, uvicorn, a real editor, git, the textbook, and — this is Phase 2 — agentic AI.
AI: Phase 2 (wk 9–16): agentic AI is ON. An agent-log.txt is REQUIRED: every task you delegated to an agent, what it did, where it went wrong, and where you intervened. This project is deliberately shaped so an agent cannot finish it alone — the Hard-tier stack decision is judgment only you can supply.
The Setup
The same small ministry from Week 9 now has a working Node JSON API for its memory-verse trainer (or prayer-request board — whatever resource you chose last week). It works. It is in production. It is fine.
Then the ministry hires a second developer. She does not write JavaScript. She writes Python — she came from a data-science background, and she is already muttering about wanting to add a feature that scores how well a member has memorized a verse using a small Python model. She looks at the Node service and asks the question every architect eventually faces:
“Could we have built this in Python instead? Should we have? And how would we even know?”
You cannot answer that question honestly until you have built the same thing both ways. So you are going to. You will re-implement the Week 9 API in FastAPI — same resource, same endpoints, same status codes, same behavior — and then you will write the memo that answers the developer’s question with reasons instead of preferences.
This is the felt decision at the center of the book. Not “which framework is best” (a question with no answer) but “which framework is right for this team, this data, this deadline” (a question with a defensible answer, once you have the constraints in front of you).
Learning Targets
By completing this project, you will demonstrate that you can:
- Build a complete CRUD JSON API in FastAPI from zero, with correct REST semantics and status codes.
- Express request/response contracts as Pydantic models — specifications the framework enforces.
- Reproduce a known behavior in a new stack and verify behavioral equivalence.
- Validate query parameters by type and raise clean
404/400/409errors. - Read and use the auto-generated OpenAPI
/docsas a contract surface. - Make and defend a right-tool stack decision under explicit constraints.
- Direct an agent to build the mechanical parts while keeping the judgment in your own hands, and document the division honestly in an agent log.
Normal Tier
Goal: Re-implement your Week 9 Node API in FastAPI. It must be behaviorally identical to the Node version, use Pydantic models as the request/response contracts, and serve a working /docs page.
Required features
- Same resource, same endpoints as your Week 9 API. At minimum:
GET /<resource>— list all.GET /<resource>/{id}— read one.POST /<resource>— create (server assigns the id), returns201.GET /health— returns{"status": "ok"}.
- Pydantic models as contracts: a
*Createmodel for the request body (noid) and a response model (idincluded), withFieldconstraints (min_length,max_length,ge, etc.) on every field where a constraint is real. - Correct status codes:
200on read,201on create (with the created resource in the body),404on a missing resource. The codes must match the table in §10.1 and match what your Node version returned. response_modeldeclared on every route that returns data, so the output shape is documented and filtered.- A working
/docspage at/docsthat shows every endpoint, every parameter, and every model. Include a screenshot of it in your repo. - Behavioral equivalence: a short
EQUIVALENCE.docx(or a section in your README) listing, endpoint by endpoint, that the FastAPI version returns the same status code and same JSON shape as the Node version. Where they differ (e.g., FastAPI’s422vs your Node’s hand-rolled400for bad input), say so and explain why.
Example interaction
$ curl -s localhost:8000/verses/1
{"id":1,"reference":"John 3:16","text":"For God so loved the world...","translation":"ESV"}
$ curl -s -X POST localhost:8000/verses \
-H "Content-Type: application/json" \
-d '{"reference":"Romans 8:28","text":"And we know that for those who love God..."}'
{"id":3,"reference":"Romans 8:28","text":"And we know that for those who love God...","translation":"ESV"}
# -> HTTP 201
$ curl -s -o /dev/null -w "%{http_code}\n" localhost:8000/verses/999
404
Normal-tier rubric (out of 100)
| Criterion | Points |
|---|---|
App runs with uvicorn main:app and serves all required endpoints | 10 |
Pydantic create model (no id) + response model (with id), with Field constraints | 16 |
GET list and GET by id correct | 10 |
POST creates, server-assigns id, returns 201 + the resource | 12 |
| Correct status codes throughout (200/201/404), matching Node version | 10 |
response_model declared and output filtered correctly | 8 |
Working /docs page (screenshot in repo) | 8 |
EQUIVALENCE.docx documenting behavioral parity with the Node version | 10 |
requirements.txt pins fastapi + uvicorn; project installs cleanly | 4 |
README + agent-log.txt present and honest | 12 |
Medium Tier (+up to 25% extra credit)
M1. Query parameters: filter and paginate (typed)
Extend GET /<resource> with validated query parameters:
- A filter parameter relevant to your resource (e.g.,
?translation=ESV, or?urgent=true), optional. limit(default 50,ge=1,le=100) andoffset(default 0,ge=0) for pagination.
All three must be declared with types/Query constraints so that bad values (?limit=0, ?limit=banana) return a clean 422. Demonstrate each in your README with a curl example.
M2. Full CRUD with proper error handling
Add PUT /<resource>/{id} (full replace, 200 on success, 404 if absent) and DELETE /<resource>/{id} (204 no content, 404 if absent). Add at least one business-rule error raised by you with HTTPException — e.g., a uniqueness constraint returning 409 Conflict on a duplicate. Show, in your README, the difference between a Pydantic 422 (shape) and your hand-raised 400/409 (business rule).
M3. The docs reflect every constraint
Open /docs and confirm that every constraint you declared — every min_length, every ge/le, every default, every status code — is visible in the generated documentation. Add description=/examples= to your Fields and Querys so the docs read well. Include a second screenshot showing an expanded endpoint with its constraints. The deliverable is: the documentation is generated, complete, and would let a stranger use your API without reading your code.
Hard Tier (+up to 25% additional extra credit)
H1. The Node-vs-FastAPI memo (the architect’s deliverable)
Write MEMO.docx: a one-to-two-page architectural memo comparing Node and FastAPI for this API under three different hypothetical constraint scenarios. For each scenario, recommend one stack and defend the recommendation with the constraint, not with taste:
- The team knows only JavaScript (three front-end engineers, no Python experience, simple API).
- Heavy data-validation needs (large, deeply nested request bodies; a bad write to the store is expensive to undo).
- Must integrate with a Python ML model (the API exists partly to serve predictions from a model that already exists in Python).
For each scenario your recommendation must cite the specific row(s) of the §10.7 comparison table that decide it. A recommendation with no constraint behind it scores zero for that scenario.
H2. Where the agent stopped and you decided
In the same MEMO.docx, add a section titled “Agent vs Architect.” For each of the three scenarios, state plainly:
- What an agent could have built (the mechanical FastAPI or Node implementation).
- What the agent could not decide (which stack this scenario deserves, and why).
This is the heart of the project. The agent builds what you specify. The whole memo is the part it cannot do for you, because it requires sitting with constraints and exercising judgment. Make that boundary explicit and concrete — quote the moment in your own process where you stopped delegating and started deciding.
H3. Equivalence under fire (optional within Hard)
Write a tiny test (use FastAPI’s TestClient, which needs no running server) that asserts the FastAPI version matches the Node behavior on at least four cases: a successful GET, a 404, a 201 create, and a 422 on bad input. Confirm the status codes and the JSON shapes. Include the test file and its passing output.
Submission
Submit one URL via the course portal: a public GitHub repo.
What the repo must contain
main.py— the FastAPI application.models.py— your Pydantic models (you may keep them inmain.pyfor the Normal tier, but splitting them out is the better shape).requirements.txt— pinningfastapianduvicorn.EQUIVALENCE.docx— endpoint-by-endpoint parity with your Node version.- A
/docsscreenshot (and a second one for M3). MEMO.docx— for Hard tier (H1 + H2).README.txt— your reflection:
# Project 10 — The Same API, in FastAPI
**Tier targeted:** Normal / Medium / Hard
**Resource:** (verses / prayer requests / ...)
**Endpoints:** (list them with methods + status codes)
**Pydantic models:** (one line each — create / response / update)
**Right tool here:** (one sentence: if you were starting fresh, Node or FastAPI for THIS ministry, and why)
**What I learned:** (one paragraph)
**What I'd change:** (one sentence)
**AI usage:** See agent-log.txt. Signed: <your name>
agent-log.txt— REQUIRED. Every task you delegated to an agent, in this format:
# Agent Log — Project 10
## Task: scaffold the FastAPI CRUD routes from the Pydantic models
- **Delegated:** "Generate GET/POST/PUT/DELETE routes for the Verse resource using these models."
- **Agent did:** produced all five routes; used `.dict()` (Pydantic v1) in two places.
- **Where it went wrong:** v1 syntax on a v2 install; also returned 200 on create, not 201.
- **Where I intervened:** switched to `.model_dump()`, set `status_code=201` on POST, verified against /docs.
## Task: (the next one...)
...
## Decisions the agent did NOT make (and could not)
- Which stack each Hard-tier scenario deserves — that judgment is in MEMO.docx, and it is mine.
What the repo must NOT do
Do not commit a .venv/ or __pycache__/. Add a .gitignore. The grader will pip install -r requirements.txt in a fresh environment and run uvicorn main:app.
Hints (Read Before You Begin)
- Build it raw first if you must, then let the agent help. You already built this in Node by hand. Stand up the FastAPI skeleton yourself — the three-line app, then one route — before you delegate the rest. You direct an agent better when you have walked the first hundred yards yourself.
- Split your models early. The
*Createmodel (no id) and the response model (id) are the contract. Getting that split right makes every route fall out naturally and forbids the “client sets its own id” bug. - Use
/docsas your test harness for the Normal tier. The “Try it out” buttons send real requests. You can verify every endpoint without writingcurlcommands or a test file. Savecurlfor the README examples. - Pydantic v2, always.
.model_dump(),ConfigDict,@field_validator. If the agent hands you.dict()orclass Config:, it copied a v1 tutorial — fix it and note it in the agent log. - The memo is the project. The code is conditioning at this point. The grader weights
MEMO.docxheavily because the constraint decision is the one thing the agent cannot do for you. Write it with specific constraints and specific table rows, not adjectives. - Behavioral equivalence has honest exceptions. FastAPI returns
422for shape errors where your Node code returned a hand-rolled400. That is a real difference. Document it; do not hide it. The lesson is that the framework draws the400/422line for you, and draws it correctly.
What Mastery Looks Like (Beyond the Rubric)
A great Project 10 reads like the same API wearing different clothes. Open the Node version and the FastAPI version side by side, and the behavior lines up endpoint for endpoint — but the FastAPI version is shorter, and the validation you wrote by hand in Node has become a Pydantic model that the framework enforces and documents for free. The student who did this well can point at any line of the FastAPI version and say what the framework is doing there, because they did it by hand last week.
A great agent-log.txt is honest about where the agent was wrong. An agent will cheerfully hand you Pydantic v1 syntax, return 200 where 201 belongs, or invent a query parameter you never asked for. The log that says “it did X, which was wrong, and here is how I caught it” demonstrates the one skill this whole book is about: keeping judgment in human hands while the machine does the typing.
And a great MEMO.docx does not hedge. It says, for each scenario, this stack, because this constraint — and it would change its answer if the constraint changed. That is the architect’s voice. “It depends” is only a good answer if you can finish the sentence: it depends on whether the team can read Python, and they can’t, so: Node.
Coach’s Note — The temptation this week is to let the agent build the whole thing in four minutes and call it done. The Normal tier will even pass if you do that. But the project is not the code — you have built this API before, in a harder language, by hand. The project is the memo and the log: the proof that you can direct the machine to build and still own every decision that mattered. An agent that ships a working API you cannot defend is not a win. It is a liability with good test coverage.
When You’re Done
- Run
uvicorn main:appand click through every endpoint in/docs. Confirm the status codes match your Node version. - Send bad input on purpose. Confirm the
422body names the broken field. - Re-read your
MEMO.docx. For each scenario, is the recommendation tied to a constraint, or to a preference? Cut every adjective that is not load-bearing. - Re-read your
agent-log.txt. Did you record where the agent was wrong? If every entry says the agent was perfect, you were not reading its output carefully enough. - Push, confirm a clean clone installs and runs, submit.
- Read Chapter 11. SQLite next — your in-memory dict finally learns to survive a restart, and you meet your first real database.
A theological footnote. A Pydantic model is a small confession: this, and not that, is what a valid request is. The church wrote its confessions for the same reason — not to be clever, but so that the promise could not be quietly bent later, so that yes meant yes and no meant no in words too precise to twist. Our Lord asks plain speech of us in Matthew 5 because plain speech is honest speech; the elaboration and the hedge are where deceit hides. When you write a contract that says exactly what your API requires and exactly what it returns — no more, no less — you are practicing that honesty in the medium God gave you to work in. Let your yes be yes.
See you next week.