0%

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 bodies and tests.

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=N answers with the first N books in id order, for N from 1 to 100. Without limit, it every 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 sends it: whatever 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 , turns it into an int, and checks that it is at least 1 (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=two get 422. Your server answered 400.

  • ?limit= with nothing after it gets 422. Your server read an empty as no limit and listed every book.

  • ?limit=1&limit=2 gets 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

  1. delete_book, about four lines: the 404 for an unknown id, then .pop().

  2. limit in list_books, about three changed lines: keep adding books while limit is None or the list is still shorter than limit.

  3. Lesson 5’s two tests in tests/test_app.py, each with fresh_client(): test_delete_unknown_book_is_404 asserts that DELETE /books/99 answers 404, and test_limit_returns_the_first_books asserts that GET /books?limit=1 answers 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 . Where the two still differ, FastAPI decided, on the versions this course uses:

RequestYour serverThe app
HEAD /books200, no body405, allow: GET
DELETE /books405, allow: GET, HEAD, POST405, allow: GET
GET /books/abc404422
GET /books/301 to /books307 to an absolute
a book labeled text/plain, or with no content-type415422
that does not parse400422
?limit=0400422
?limit=1&limit=2400200, 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 in : write delete_book, make list_books honor limit, and write Lesson 5’s two tests.

In reading_app.py:

  1. delete_book(book_id) raises HTTPException(status_code=404, detail="no such book") when STATE["books"] has no book with that id. Otherwise it removes the book with .pop(). The supplied status_code=204 makes the reply a 204 with no body.

  2. list_books(limit) returns {"books": [...]} with the first limit books in id order, or every book when limit is None. FastAPI has already checked that limit is None or 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():

  1. test_delete_unknown_book_is_404: DELETE /books/99 answers 404.

  2. test_limit_returns_the_first_books: GET /books?limit=1 answers 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 directly. FastAPI’s own answers, such as 422 for ?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.