What a Framework Does for You · capstone
Capstone project: Project: The Reading List in FastAPI
Your app answers GET on both paths, writes books from a checked model, and runs its tests in process. Two behaviors of your Chapter 5 server are still missing: DELETE /books/{id}, and the limit query on GET /books. This project adds both, with two tests that notice when either one breaks. Every @app line and BookIn are already in reading_app.py; you write
What the app must do
DELETE /books/{id}removes the book and answers 204 with no body. An unknown id gets 404, as everywhere else in the app.GET /books?limit=Nanswers with the first N books in id order, for N from 1 to 100. Withoutlimit, itevery book, and a limit larger than the number of books lists every book too.
204 with nothing
The line above delete_book is @app.delete("/books/{book_id}", status_code=204). A 204 says the request succeeded and there is nothing to send back. In Chapter 5 you built that reply by hand, with no body and no content-length. Here delete_book returns, the 204 goes out with no body and no content-length. On these versions it still carries content-type: application/json, a label with nothing to label, which is harmless. So the function’s job is the decision. Is there such a book? If not, raise the same HTTPException as get_book. If there is, remove it with .pop(), as your server’s delete_book did.
delete_book ends with return {"deleted": 7} under status_code=204. What is sent?
A query parameter FastAPI checks
Now read the rest of list_books’s def line:
def list_books(limit: int | None = Query(default=None, ge=1, le=100)):
Read it as you read book_id: int. FastAPI takes limit from the query ge, greater than or equal) and at most 100 (le, less than or equal), all before your function runs. int | None with default=None means a request may leave it out, and then limit is None. So your function only ever meets None or a whole number from 1 to 100, and it needs no checks of its own.
FastAPI answers the rest itself, and not always as your server did:
?limit=0,?limit=101, and?limit=twoget 422. Your server answered 400.?limit=with nothing after it gets 422. Your server read an emptyas no limit and listed every book. ?limit=1&limit=2gets 200 with two books: FastAPI keeps the last value of a repeated. Your server refused the repeat with 400.
GET /books?limit=1&limit=2. Chapter 5 answered 400. What does the app answer?
Milestones
delete_book, about four lines: the 404 for an unknown id, then.pop().limitinlist_books, about three changed lines: keep adding books whilelimitisNoneor the list is still shorter thanlimit.Lesson 5’s two tests in
tests/test_app.py, each withfresh_client():test_delete_unknown_book_is_404asserts thatDELETE /books/99answers 404, andtest_limit_returns_the_first_booksasserts thatGET /books?limit=1answers with exactly one book, book 1. A third test that deletes a book and then gets 404 when it asks for that book is worth adding.
Run the tests in the Shell tab:
python -m pytest -q
Every test should pass, and the summary line ends with 1 warning, the anyio notice from Lesson 2. If you like, run python pl_http.py uvicorn again: its three replies are the ones Lesson 4 showed.
Check my work sends requests through TestClient with books you have not seen, repeats the checks of Lessons 1 and 3, and checks DELETE, unknown ids on GET, PUT and DELETE, and limit. Then it runs your tests three times, as in Lesson 2: against your own app, where all must pass, and against two versions built from your functions with one mistake each, a delete_book that answers 204 for an unknown id and a list_books that ignores limit. One of your tests must fail against each.
The app beside your server
The app now matches your standard-library server on every behavior its functions decide: the statuses they choose, location, the stored books, 404 for an unknown id, and the limit
| Request | Your server | The app |
|---|---|---|
HEAD /books | 200, no body | 405, allow: GET |
DELETE /books | 405, allow: GET, HEAD, POST | 405, allow: GET |
GET /books/abc | 404 | 422 |
GET /books/ | 301 to /books | 307 to an absolute |
a book labeled text/plain, or with no content-type | 415 | 422 |
| 400 | 422 | |
?limit=0 | 400 | 422 |
?limit=1&limit=2 | 400 | 200, two books |
Changing those answers back would take code that catches FastAPI’s own errors, which this course does not ask you to write. Your tests record the ones that matter to you instead, and they will tell you if a new version of FastAPI changes one.
Press Check my work. One contract now has two implementations, and Chapter 12 returns to the one you built by hand: you finish it, finish its requests client, and prove that the two agree.
Task
Finish the reading delete_book, make list_books honor limit, and write Lesson 5’s two tests.
In reading_app.py:
delete_book(book_id)raisesHTTPException(status_code=404, detail="no such book")whenSTATE["books"]has no book with that id. Otherwise it removes the book with.pop(). The suppliedstatus_code=204makes the reply a 204 with no body.list_books(limit)returns{"books": [...]}with the firstlimitbooks in id order, or every book whenlimitisNone. FastAPI has already checked thatlimitisNoneor a whole number from 1 to 100.
Keep get_book, create_book, and replace_book as Lessons 1 and 3 left them, and every @app line, def line, and BookIn as they came.
In tests/test_app.py, replace the two placeholders, each sending its request through fresh_client():
test_delete_unknown_book_is_404:DELETE /books/99answers 404.test_limit_returns_the_first_books:GET /books?limit=1answers with exactly one book, book 1.
Keep the rules of Lesson 2: import reading_app, no from reading_app import ..., no TestClient(...) or stored reading_app.app at the top of the file, and no socket, http, urllib, uvicorn, requests, or httpx import. You may add more tests.
In the python -m pytest -q runs every test; all should pass. python pl_http.py uvicorn is there to watch.
Then press Check my work. It fills STATE through reset_state with books you have not seen and sends requests into the app in this process with TestClient: the reads of Lesson 1, the writes of Lesson 3 with values the model accepts and values it refuses, DELETE of a book that exists and of one that does not, GET and PUT of an unknown id, and limit from 1 upward. It also calls your ?limit=0 and 405 for HEAD /books, are checked as FastAPI’s. Then it runs your tests three times: against your own app, where at least five must pass and none may fail, and against two versions of the app built from your functions with one mistake each, a delete_book that answers 204 for an unknown id and a list_books that ignores limit. At least one test must fail against each. A run that takes longer than 3 seconds is stopped and reported. Nothing from your terminal is graded.