Project 10

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/409 errors.
  • Read and use the auto-generated OpenAPI /docs as 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

  1. 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), returns 201.
    • GET /health — returns {"status": "ok"}.
  2. Pydantic models as contracts: a *Create model for the request body (no id) and a response model (id included), with Field constraints (min_length, max_length, ge, etc.) on every field where a constraint is real.
  3. Correct status codes: 200 on read, 201 on create (with the created resource in the body), 404 on a missing resource. The codes must match the table in §10.1 and match what your Node version returned.
  4. response_model declared on every route that returns data, so the output shape is documented and filtered.
  5. A working /docs page at /docs that shows every endpoint, every parameter, and every model. Include a screenshot of it in your repo.
  6. 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’s 422 vs your Node’s hand-rolled 400 for 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)

CriterionPoints
App runs with uvicorn main:app and serves all required endpoints10
Pydantic create model (no id) + response model (with id), with Field constraints16
GET list and GET by id correct10
POST creates, server-assigns id, returns 201 + the resource12
Correct status codes throughout (200/201/404), matching Node version10
response_model declared and output filtered correctly8
Working /docs page (screenshot in repo)8
EQUIVALENCE.docx documenting behavioral parity with the Node version10
requirements.txt pins fastapi + uvicorn; project installs cleanly4
README + agent-log.txt present and honest12

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) and offset (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:

  1. The team knows only JavaScript (three front-end engineers, no Python experience, simple API).
  2. Heavy data-validation needs (large, deeply nested request bodies; a bad write to the store is expensive to undo).
  3. 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

  1. main.py — the FastAPI application.
  2. models.py — your Pydantic models (you may keep them in main.py for the Normal tier, but splitting them out is the better shape).
  3. requirements.txt — pinning fastapi and uvicorn.
  4. EQUIVALENCE.docx — endpoint-by-endpoint parity with your Node version.
  5. A /docs screenshot (and a second one for M3).
  6. MEMO.docx — for Hard tier (H1 + H2).
  7. 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>
  1. agent-log.txtREQUIRED. 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 *Create model (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 /docs as your test harness for the Normal tier. The “Try it out” buttons send real requests. You can verify every endpoint without writing curl commands or a test file. Save curl for the README examples.
  • Pydantic v2, always. .model_dump(), ConfigDict, @field_validator. If the agent hands you .dict() or class 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.docx heavily 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 422 for shape errors where your Node code returned a hand-rolled 400. That is a real difference. Document it; do not hide it. The lesson is that the framework draws the 400/422 line 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

  1. Run uvicorn main:app and click through every endpoint in /docs. Confirm the status codes match your Node version.
  2. Send bad input on purpose. Confirm the 422 body names the broken field.
  3. 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.
  4. 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.
  5. Push, confirm a clean clone installs and runs, submit.
  6. 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.