Build a Book Collection API with FastAPI

Build a CRUD REST API in Python with automatic data validation and docs.

Introduction

30 Second Summary

Every time you search for a book online, check its availability, or browse recommendations, a behind-the-scenes system is receiving your request, looking up the data, and sending back a response. That system is an API.

In this project, you will build a Book Collection API from scratch using Python and FastAPI. You will create endpoints that let users add, view, update, and delete books, with automatic data validation and interactive documentation built in.

What You'll Build

A fully functional Book Collection REST API that performs CRUD operations on an in-memory book list, validates incoming data with Pydantic models, and documents itself automatically through Swagger UI.

By the end of this project, you'll have:

  • A working REST API running locally that handles full Create, Read, Update, and Delete operations on a book collection.
  • Hands-on experience using Pydantic models to validate request data automatically with zero manual checking.
  • Explored FastAPI's interactive Swagger UI documentation to test every endpoint without any external tools.
  • Secret Mission: Add a review system where users can post ratings and comments on books.

Are there any prerequisites?

You should be comfortable with basic Python concepts like variables, functions, lists, and dictionaries. No prior experience with web frameworks or APIs is needed.

Before We Start

Before We Start...

Set Up Your Development Environment

Every web application starts with a server that listens for requests and sends back responses. To build yours, you need Python (the language) and FastAPI (the framework that makes building APIs fast and painless).

In this step, you will install Python, create an isolated project environment, and install FastAPI with all its dependencies.

In this step, get ready to:
  • Install Python 3.14 and verify it works.
  • Create a project folder with a virtual environment.
  • Install the FastAPI framework.
Install Python 3.14

Python is the programming language that FastAPI is built on. You need version 3.14 or higher to use the latest syntax features this project relies on.

  • Check if Python is already installed by running this command in your terminal:

macOS

python3 --version

Windows

python --version

Check your output against the tabs below.

✔️ I see version 3.14 or higher

You already have the right version of Python installed. Skip ahead to the next substep.

ⓧ I see an older version

Your system has an older Python that will not support this project's syntax. You need to install Python 3.14 alongside your existing version.

  • Visit python.org/downloads and download the Python 3.14 installer for your operating system.
  • Run the installer. On Windows, check the box that says Add python.exe to PATH before clicking Install Now.
  • On macOS, if python3 --version still shows the old version after installing, use the full path python3.14 --version to confirm the new install works. You can also use a version manager like pyenv to switch your default.
  • Close and reopen your terminal, then re-run the version check command to confirm you see 3.14 or higher.

Still seeing the old version on macOS?

macOS ships with an older system Python that shadows new installs. Try running python3.14 directly, or install pyenv from https://github.com/pyenv/pyenv to manage versions.

help me get Python 3.14 working on my Mac

ⓧ Command not found

Python is not installed on your machine yet.

  • Visit python.org/downloads and download the Python 3.14 installer for your operating system.
  • Run the installer. On Windows, check the box that says Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal after the installation completes.
  • Run the version check command again to confirm you see 3.14 or higher.

Still getting command not found?

On Windows, the installer may not have added Python to your PATH. Re-run the installer, select Modify, and make sure Add python.exe to PATH is checked.

On macOS, try python3 instead of python. The macOS installer registers the command as python3.

help me install Python 3.14

Create a project folder and virtual environment

A virtual environment is an isolated copy of Python for your project. It keeps your project's packages separate from your system's Python packages, so different projects never conflict.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir book-collection-api
cd book-collection-api
  • Confirm the folder exists by running:
ls

The output should be empty since you just created the folder. That is expected.

  • Create a virtual environment by running this command:

macOS

python3 -m venv venv

Windows

python -m venv venv

What does this command do?

python -m venv venv tells Python to run its built-in venv module, which creates a virtual environment in a folder called venv inside your project.

Any packages you install while the virtual environment is active only live inside this folder. Your system Python stays untouched.

  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, if you get a permissions error, run this command first and then try activating again:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

On macOS, make sure you typed source before the path. Without it, the command will not work.

help me activate my Python virtual environment

  • Open VS Code (or Cursor) in your project folder. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code (or Cursor) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the book-collection-api folder, then click Open.

Use VS Code's built-in terminal

Once your folder is open in VS Code, you can use its integrated terminal instead of a separate terminal window. Open it with Ctrl+` (both macOS and Windows). If you do, re-activate your virtual environment inside that terminal before continuing.

Install FastAPI

FastAPI is the Python framework you will use to build your API. Installing it with the [standard] extras includes everything you need to run a development server.

  • Make sure your virtual environment is still active (you should see (venv) in your prompt).
  • Install FastAPI and its dependencies by running:
pip install "fastapi[standard]"

What does this install?

The [standard] extras install FastAPI plus Uvicorn (the server that runs your app), the FastAPI CLI, and other useful tools. Without [standard], you would need to install the server separately.

You will see several packages downloading and installing. Wait until it finishes and you see your prompt again.

  • Verify the installation by running:
fastapi --version

You should see a version number printed in your terminal. This confirms the fastapi CLI is installed and working.

Seeing 'command not found' for fastapi?

Make sure your virtual environment is active (you should see (venv) in your prompt). If it is not active, re-run the activation command from the previous substep.

If the environment is active but the command still fails, try deactivating and reactivating:

deactivate
source venv/bin/activate

On Windows, replace the second line with venv\Scripts\activate.

help me fix the fastapi command not found error

Your development environment is ready to go. Next up, you will write your first API endpoint and see it running in the browser.

Create Your First Endpoint

Your development environment is ready and FastAPI is installed. Now it's time to write actual code that responds when someone visits a URL on your server.

An endpoint is a specific URL path that your API listens on. When a request hits that path, your server runs a function and sends back a response. In this step, you'll create two endpoints and see them working live in your browser.

In this step, get ready to:
  • Create your first API endpoint that returns a welcome message.
  • Run the development server and explore the automatic Swagger UI documentation.
  • Add a second endpoint that returns a list of books.
Create main.py with a root endpoint

Every FastAPI app starts with two things: an import and an app instance. The app instance is the core object that holds all your routes and configuration.

  • In VS Code, create a new file called main.py in your project folder by clicking **File** > **New File**, then saving it as main.py.
  • Add the following code to main.py:
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Welcome to the Book Collection API"}

What does this code do?

  • from fastapi import FastAPI imports the FastAPI class from the library you installed.
  • app = FastAPI() creates your application instance. All your endpoints attach to this object.
  • @app.get("/") is a decorator that tells FastAPI to call the function below whenever someone sends a GET request to the root URL /.
  • The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
  • Save main.py.
Run the development server and explore Swagger UI

FastAPI comes with a built-in development server that auto-reloads whenever you change your code. It also generates interactive API documentation automatically.

  • Start the development server by running this command in your terminal:
fastapi dev main.py

What does this command do?

fastapi dev starts Uvicorn (an async web server) with auto-reload enabled. Every time you save main.py, the server restarts automatically so you see your changes immediately.

  • Open your browser and visit http://127.0.0.1:8000.

You should see a JSON response that says {"message": "Welcome to the Book Collection API"}. That's your first endpoint working.

  • Now visit http://127.0.0.1:8000/docs in your browser.

This is the Swagger UI page. FastAPI automatically generates this interactive documentation from your code. You can see your GET / endpoint listed, and you can click Try it out to execute it directly from the browser.

No need for Postman

Many API tutorials ask you to install a separate tool like Postman to test endpoints. FastAPI's built-in Swagger UI at /docs does the same thing. You'll use it for the rest of this project.

Page not loading?

  • Make sure your terminal is still running the fastapi dev main.py command. If you closed it, run it again.
  • Check for typos in the URL. It must be http://127.0.0.1:8000 with no trailing slash issues.
  • If you see a "port already in use" error, another process is using port 8000. Close other terminal windows and try again.

Why can't I see my API in the browser?

Add a /books endpoint

A real API serves data. Let's add a second endpoint that returns a list of books as JSON. For now, you'll hardcode the data directly in the function.

  • In main.py, add the following code below your existing read_root function:
@app.get("/books")
def get_books():
    return [
        {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "genre": "fiction", "year": 1925},
        {"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "genre": "technology", "year": 2008},
    ]

What does this code do?

  • @app.get("/books") registers a new GET endpoint at the /books path.
  • The function returns a Python list of dictionaries. FastAPI converts this into a JSON array automatically.
  • Each dictionary represents one book with fields for id, title, author, genre, and year.
  • Save main.py. The development server auto-reloads, so your new endpoint is already live.
  • Switch back to your browser and refresh the Swagger UI at http://127.0.0.1:8000/docs.
  • You should now see two endpoints listed: GET / and GET /books.
  • Click on the GET /books endpoint, then click Try it out, then Execute.

You should see a JSON response containing both books with their titles, authors, genres, and years.

Only seeing the old endpoint?

  • Check your terminal for error messages. A syntax error in your new code prevents the server from reloading.
  • Make sure the new function is outside and below the read_root function, not indented inside it.
  • Hard-refresh the browser with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached results.

My /books endpoint isn't showing up in Swagger UI after I saved the file.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and both endpoints appear in Swagger UI.

ⓧ I'd like to double check the full code

Here's what your complete main.py should look like at this point:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Welcome to the Book Collection API"}


@app.get("/books")
def get_books():
    return [
        {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "genre": "fiction", "year": 1925},
        {"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "genre": "technology", "year": 2008},
    ]

You now have a working API with two endpoints serving real data. Next up, you'll define Pydantic models to structure and validate your data properly.

Define Pydantic Models for Data Validation

Your API is returning books as raw dictionaries. That works for now, but there is nothing stopping bad data from sneaking in.

In this step, you will define Pydantic models that act as a blueprint for your data. Every book that enters or leaves your API will be validated against these blueprints automatically.

In this step, get ready to:
  • Create a Book model that defines exactly what a book looks like.
  • Create a BookCreate model for incoming requests (without an ID).
  • Replace your hardcoded dictionaries with validated Book objects.
Import BaseModel and create the Book model

Pydantic's BaseModel class is the foundation for data validation in FastAPI. When you define a class that inherits from BaseModel, every field you declare becomes a validation rule.

  • In main.py, find the import line at the top of the file:
from fastapi import FastAPI
  • Add this new import directly below it:
from pydantic import BaseModel
  • Now add the Book model below the app = FastAPI() line by copying this code:
class Book(BaseModel):
    id: int
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None

What does this code do?

Each line inside the class declares a field name and its expected type. For example, title: str means the title must be a string. If someone sends an integer for the title, FastAPI will reject the request automatically.

The rating: float | None = None line means rating is optional. It can be a decimal number or None, and it defaults to None if not provided.

Create the BookCreate model

When someone adds a new book to your API, they should not have to provide an ID. The server should assign IDs automatically. A separate model without the id field handles this cleanly.

  • Add the BookCreate class above the Book class you just wrote (between app = FastAPI() and class Book):
class BookCreate(BaseModel):
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None

Why two separate models?

BookCreate defines what a user sends when creating a book. Book defines what the API returns, which includes the server-assigned id. This separation keeps your API predictable.

The user never chooses their own ID. The server is always in control of that value.

  • Save main.py.

Seeing a red underline or syntax error?

Make sure BookCreate is placed above Book in your file, and that both classes start with class (lowercase). Check that every field line is indented with 4 spaces.

help me fix a syntax error in my Pydantic model

Replace the hardcoded list with Book objects

Your /books endpoint currently returns plain dictionaries. Now that you have proper models, you can store real Book objects that enforce your data rules.

  • Find the @app.get("/") def read_root() function in your file. Add the following code above that function (below your model classes):
# In-memory storage for books
books: list[Book] = [
    Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", genre="fiction", year=1925, rating=4.5),
    Book(id=2, title="Clean Code", author="Robert C. Martin", genre="technology", year=2008, rating=4.7),
    Book(id=3, title="Sapiens", author="Yuval Noah Harari", genre="non-fiction", year=2011, rating=4.6),
]

# Tracks the next available ID for new books
next_id = 4

What does this code do?

books: list[Book] declares a list that can only hold Book objects. Each Book(...) call creates a validated book instance. If you accidentally passed a string for year, Pydantic would raise an error immediately.

The next_id variable tracks what ID to assign the next time someone creates a book. Since you already have 3 books (IDs 1, 2, 3), the next one gets ID 4.

Now update the /books endpoint to use this list instead of the hardcoded dictionaries.

  • Find your existing get_books function:
@app.get("/books")
def get_books():
    return [
        {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "genre": "fiction", "year": 1925},
        {"id": 2, "title": "Clean Code", "author": "Robert C. Martin", "genre": "technology", "year": 2008},
    ]
  • Replace the entire function (including the decorator line above it) with this updated version:
@app.get("/books", response_model=list[Book])
def get_books():
    return books

What changed here?

response_model=list[Book] tells FastAPI that this endpoint always returns a list of Book objects. FastAPI uses this to validate outgoing data and generate accurate API documentation.

The function body now returns the books list you defined above, instead of hardcoded dictionaries.

  • Save main.py.
  • Switch to your terminal. If the server is still running, it will auto-reload. If not, start it again by running:
fastapi dev main.py
  • Visit http://127.0.0.1:8000/books in your browser. You should see three books, each with a rating field included.

Seeing an error when the server reloads?

Check that you deleted the entire old get_books function including its decorator. Having two @app.get("/books") decorators in the same file causes a conflict.

Also verify that the books list is defined above the @app.get("/") def read_root() function, not inside it.

help me fix an error after adding Pydantic models to my FastAPI app

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and your browser shows three books with ratings.

ⓧ I'd like to double check the full code

Here is the complete main.py at this point:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BookCreate(BaseModel):
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


class Book(BaseModel):
    id: int
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


books: list[Book] = [
    Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", genre="fiction", year=1925, rating=4.5),
    Book(id=2, title="Clean Code", author="Robert C. Martin", genre="technology", year=2008, rating=4.7),
    Book(id=3, title="Sapiens", author="Yuval Noah Harari", genre="non-fiction", year=2011, rating=4.6),
]

next_id = 4


@app.get("/")
def read_root():
    return {"message": "Welcome to the Book Collection API"}


@app.get("/books", response_model=list[Book])
def get_books():
    return books

Your data now has structure and validation baked in. Next up, you will build the full set of CRUD endpoints so users can create, read, update, and delete books through your API.

Build the Full CRUD Endpoints

Your API can return a list of books, but a real-world API needs to do more. Users need to add new books, look up a single book by its ID, update book details, and remove books they no longer want.

These four operations (Create, Read, Update, Delete) form the CRUD pattern. In this step, you'll build the remaining endpoints that complete your Book Collection API.

In this step, get ready to:
  • Add a POST endpoint to create new books.
  • Add a GET endpoint to retrieve a single book by ID.
  • Add PUT and DELETE endpoints to update and remove books.
Add a POST endpoint to create new books

When someone sends a POST request to your API, they want to add a new book to the collection. Your endpoint needs to accept the book data, assign a unique ID, and store it.

  • In main.py, add the following code below your existing get_books function:
@app.post("/books", response_model=Book, status_code=201)
def create_book(book_data: BookCreate):
    global next_id
    # Create a new Book with an auto-assigned ID
    new_book = Book(id=next_id, **book_data.model_dump())
    books.append(new_book)
    next_id += 1
    return new_book

What does this code do?

This endpoint handles POST requests to /books. The status_code=201 tells the client that a new resource was successfully created.

The book_data: BookCreate parameter tells FastAPI to expect a JSON request body matching the BookCreate model. FastAPI validates the incoming data automatically.

The global next_id keyword lets the function modify the module-level next_id variable. Each new book gets the current ID, then next_id increments so the next book gets a unique number.

  • Save main.py.
  • Switch to your browser and go to http://127.0.0.1:8000/docs.
  • Click on the green POST /books row to expand it.
  • Click Try it out.
  • Replace the example JSON body with this:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "genre": "science-fiction",
  "year": 1965,
  "rating": 4.8
}
  • Click Execute. You should see a 201 response with your new book, including "id": 4 assigned by the server.

Seeing a 422 Validation Error?

A 422 response means the request body did not match the BookCreate model. Check that your JSON includes all required fields (title, author, genre, year) and that year is an integer, not a string.

help me fix a 422 validation error on my POST /books endpoint

Add a GET endpoint to retrieve a single book

Returning all books is useful, but users also need to fetch one specific book by its ID. This is where path parameters come in.

You also need a way to tell the client when a book does not exist. FastAPI provides HTTPException for returning error responses with proper status codes.

  • In main.py, find the import line at the top of the file:
from fastapi import FastAPI
  • Replace it with the following to add HTTPException:
from fastapi import FastAPI, HTTPException
  • Now add the following endpoint below your create_book function:
@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    # Search through the books list for a matching ID
    for book in books:
        if book.id == book_id:
            return book
    # If no book matched, return a 404 error
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

The {book_id} in the route path is a path parameter. FastAPI automatically extracts the value from the URL and passes it to the function as book_id: int. It also validates that the value is an integer.

If no book matches the requested ID, raise HTTPException(status_code=404) sends back a proper 404 Not Found response. The detail message is included in the JSON response body.

  • Save main.py.
  • In the Swagger UI, click on the GET /books/{book_id} row.
  • Click Try it out, enter 1 for book_id, and click Execute. You should see The Great Gatsby returned.
  • Try again with 99 for book_id. You should see a 404 response with "detail": "Book not found".

Getting a 422 instead of a 404?

Make sure you entered a number for book_id, not a word. FastAPI validates that path parameters match the declared type (int). If you pass a non-numeric value, it returns 422 instead of reaching your function.

help me understand why my GET /books/{book_id} endpoint is returning 422

Add PUT and DELETE endpoints

The last two CRUD operations let users update an existing book's details or remove a book entirely. Both need to handle the case where the requested book does not exist.

  • In main.py, add the following PUT endpoint below your get_book function:
@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, book_data: BookCreate):
    # Find the book and replace it with updated data
    for index, book in enumerate(books):
        if book.id == book_id:
            updated_book = Book(id=book_id, **book_data.model_dump())
            books[index] = updated_book
            return updated_book
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

The PUT endpoint accepts both a path parameter (book_id) and a request body (book_data: BookCreate). FastAPI knows which is which automatically. Parameters in the URL path become path parameters. Parameters typed as Pydantic models become the request body.

The enumerate() function gives both the index and the book at each position. This lets you replace the book at that exact spot in the list with the updated version.

  • Save main.py and verify: in the Swagger UI, expand PUT /books/{book_id}, click Try it out, enter 1 for book_id, update the title in the request body, and click Execute. You should see the updated book returned with "id": 1.
  • Now add the DELETE endpoint below update_book:
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    # Find and remove the book from the list
    for index, book in enumerate(books):
        if book.id == book_id:
            books.pop(index)
            return {"message": "Book deleted successfully"}
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

The DELETE endpoint finds the book by ID and uses books.pop(index) to remove it from the list. It returns a simple confirmation message.

Notice this endpoint has no response_model because it returns a plain dictionary instead of a Book object. FastAPI serialises the dictionary directly to JSON.

  • Save main.py.
  • In the Swagger UI, expand DELETE /books/{book_id}, click Try it out, enter 1 for book_id, and click Execute.
  • You should see "message": "Book deleted successfully" in the response.
  • Try the same book_id again. You should now get a 404 because the book no longer exists.

Not seeing your new endpoints in the docs?

Make sure you saved main.py. The fastapi dev server auto-reloads on file save, but unsaved changes will not appear. Refresh http://127.0.0.1:8000/docs in your browser to see the updated endpoint list.

If you see a traceback in the terminal mentioning HTTPException, confirm you updated the import line at the top to include HTTPException.

help me debug why my PUT or DELETE endpoints are not working

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.py at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class BookCreate(BaseModel):
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


class Book(BaseModel):
    id: int
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


books: list[Book] = [
    Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", genre="fiction", year=1925, rating=4.5),
    Book(id=2, title="Clean Code", author="Robert C. Martin", genre="technology", year=2008, rating=4.7),
    Book(id=3, title="Sapiens", author="Yuval Noah Harari", genre="non-fiction", year=2011, rating=4.6),
]

next_id = 4


@app.get("/")
def read_root():
    return {"message": "Welcome to the Book Collection API"}


@app.get("/books", response_model=list[Book])
def get_books():
    return books


@app.post("/books", response_model=Book, status_code=201)
def create_book(book_data: BookCreate):
    global next_id
    new_book = Book(id=next_id, **book_data.model_dump())
    books.append(new_book)
    next_id += 1
    return new_book


@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    for book in books:
        if book.id == book_id:
            return book
    raise HTTPException(status_code=404, detail="Book not found")


@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, book_data: BookCreate):
    for index, book in enumerate(books):
        if book.id == book_id:
            updated_book = Book(id=book_id, **book_data.model_dump())
            books[index] = updated_book
            return updated_book
    raise HTTPException(status_code=404, detail="Book not found")


@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    for index, book in enumerate(books):
        if book.id == book_id:
            books.pop(index)
            return {"message": "Book deleted successfully"}
    raise HTTPException(status_code=404, detail="Book not found")

Your API now supports every CRUD operation. Next up, you'll add query parameters so users can filter books by genre and minimum rating.

Add Query Parameters for Filtering

Your API can create, read, update, and delete books. But what if a user only wants fiction books, or only books rated above 4.0?

Right now, the GET /books endpoint returns every book in the collection. In this step, you will add query parameters that let users filter results without creating separate endpoints for every possible filter.

In this step, get ready to:
  • Add optional query parameters to the GET /books endpoint.
  • Implement filtering logic for genre and minimum rating.
  • Test all filters in the Swagger UI.
Update the GET /books endpoint to accept query parameters

Query parameters are key-value pairs that appear after a ? in a URL. For example, /books?genre=fiction asks the server to return only fiction books.

In FastAPI, any function parameter that is not part of the URL path is automatically treated as a query parameter. Setting the default to None makes the parameter optional.

  • In main.py, find the get_books function definition.
  • Find this line:
def get_books():
  • Replace it with this updated signature that accepts two optional query parameters:
def get_books(genre: str | None = None, min_rating: float | None = None):

What do these parameters mean?

genre: str | None = None means the user can pass a string for genre, or skip it entirely (defaults to None).

min_rating: float | None = None works the same way for a decimal number. When either parameter is None, the filter is simply skipped.

Implement the filtering logic

The function signature tells FastAPI what parameters to expect. Now you need logic inside the function to actually filter the book list based on those values.

  • Inside the get_books function, find the line that says:
    return books
  • Replace that line with the following filtering logic:
    # Start with all books
    result = books
    # Filter by genre if the user provided one (case-insensitive)
    if genre:
        result = [book for book in result if book.genre.lower() == genre.lower()]
    # Filter by minimum rating if the user provided one
    if min_rating:
        result = [book for book in result if book.rating is not None and book.rating >= min_rating]
    return result

What does this filtering code do?

result = books starts with the full collection. Each if block narrows the list only when the user provides that filter.

The .lower() calls make genre matching case-insensitive, so Fiction, fiction, and FICTION all match. The rating filter also checks that book.rating is not None to avoid comparing against books without a rating.

  • Save main.py.
  • Check the terminal where fastapi dev is running. You should see it reload without errors.

Seeing a syntax error in the terminal?

  • Make sure the def get_books(genre: str | None = None, min_rating: float | None = None): line replaced the old def get_books(): line completely. Having both lines will cause an error.
  • Check that the result = books line is indented with 4 spaces inside the function.
  • Confirm you removed the old return books line and replaced it with the filtering logic ending in return result.

Help me debug my query parameter issue

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and your server reloaded without errors.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class BookCreate(BaseModel):
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


class Book(BaseModel):
    id: int
    title: str
    author: str
    genre: str
    year: int
    rating: float | None = None


books: list[Book] = [
    Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", genre="fiction", year=1925, rating=4.5),
    Book(id=2, title="Clean Code", author="Robert C. Martin", genre="technology", year=2008, rating=4.7),
    Book(id=3, title="Sapiens", author="Yuval Noah Harari", genre="non-fiction", year=2011, rating=4.6),
]

next_id = 4


@app.get("/")
def read_root():
    return {"message": "Welcome to the Book Collection API"}


@app.get("/books", response_model=list[Book])
def get_books(genre: str | None = None, min_rating: float | None = None):
    result = books
    if genre:
        result = [book for book in result if book.genre.lower() == genre.lower()]
    if min_rating:
        result = [book for book in result if book.rating is not None and book.rating >= min_rating]
    return result


@app.post("/books", response_model=Book, status_code=201)
def create_book(book_data: BookCreate):
    global next_id
    new_book = Book(id=next_id, **book_data.model_dump())
    books.append(new_book)
    next_id += 1
    return new_book


@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    for book in books:
        if book.id == book_id:
            return book
    raise HTTPException(status_code=404, detail="Book not found")


@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, book_data: BookCreate):
    for index, book in enumerate(books):
        if book.id == book_id:
            updated_book = Book(id=book_id, **book_data.model_dump())
            books[index] = updated_book
            return updated_book
    raise HTTPException(status_code=404, detail="Book not found")


@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    for index, book in enumerate(books):
        if book.id == book_id:
            books.pop(index)
            return {"message": "Book deleted successfully"}
    raise HTTPException(status_code=404, detail="Book not found")
Test your filters in the Swagger UI

Swagger UI automatically picks up query parameters and shows them as fillable fields. You do not need any external tools to test your API.

  • Switch back to your browser and go to http://127.0.0.1:8000/docs.
  • Find the GET /books endpoint and click it to expand.
  • Click Try it out.
  • Enter fiction in the genre field and click Execute.

You should see only "The Great Gatsby" in the response, because it is the only book with genre fiction.

  • Clear the genre field.
  • Enter 4.6 in the min_rating field and click Execute.

You should see "Clean Code" (4.7) and "Sapiens" (4.6) in the response, since both have ratings at or above 4.6.

  • Enter fiction for genre and 4.0 for min_rating, then click Execute.

You should see only "The Great Gatsby" since it is the only fiction book with a rating of 4.0 or higher.

  • Test the other endpoints too (POST, GET by ID, PUT, DELETE) to confirm everything still works after your changes.

How does Swagger UI know about your parameters?

FastAPI reads your Python type hints and generates an OpenAPI schema automatically. The Swagger UI renders input fields for every declared parameter and marks optional ones.

This means your API documentation stays perfectly in sync with your code. Change a type hint and the docs update on the next reload.

Filters not working as expected?

  • If all books return regardless of filters, check that your function signature uses genre and min_rating as parameter names (not Genre or minRating).
  • If you get an empty list when filtering by genre, make sure you saved main.py and the server reloaded. Also confirm you are typing the genre in the Swagger UI field, not in the URL bar.

Help me debug my query parameter filters

Secret mission

Add a Book Review System

Your API handles books, but what about reader opinions? In this secret mission, you'll add a nested review system where users can post ratings and comments for individual books. This introduces nested resources, numeric validation constraints, and dictionary-based storage.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The FastAPI dev server (running process in your terminal).
  • Your project folder containing main.py and the venv virtual environment.

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend your API further.

  • Your FastAPI dev server continues running in the terminal. You can keep testing endpoints in the Swagger UI at http://127.0.0.1:8000/docs.
  • Your project folder stays on your Desktop with all code intact.
  • Python and FastAPI remain installed and ready for your next project.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your files stay on disk so you can pick up where you left off.

  • Switch to the terminal where your FastAPI server is running.
  • Press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate

When you come back, reactivate the virtual environment and restart the server.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code and virtual environment permanently.

  • Switch to the terminal where your FastAPI server is running.
  • Press Ctrl+C to stop the server.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder.

macOS

  • Run these commands to remove the project folder:
cd ~/Desktop
rm -rf book-collection-api

Windows

  • Run these commands to remove the project folder:
cd ~/Desktop
rmdir /s /q book-collection-api

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional REST API from scratch using FastAPI and Python.

You've learned how to:

  • Built a complete CRUD API with FastAPI, including endpoints for creating, reading, updating, and deleting books.
  • Used Pydantic models to validate incoming request data automatically using Python type hints.
  • Implemented path parameters, query parameters, and proper HTTP status codes while exploring the auto-generated Swagger UI documentation.
  • Added a nested review system with rating validation constraints using Pydantic's Field.

Ready to quiz yourself?