Build a Bookstore API with FastAPI

Build a REST API with full CRUD, validation, and pagination using FastAPI.

Introduction

30 Second Summary

Every time you order food through an app, check the weather, or scroll through social media, an API is working behind the scenes to connect everything together. Building your own is one of the most practical skills in software development.

In this project, you will build a Bookstore API from scratch using FastAPI and Python. You'll go from a single welcome message to a full set of endpoints that create, retrieve, update, and delete books.

What You'll Build

Picture yourself opening a browser tab, picking an endpoint from an interactive testing interface, and sending a request that instantly creates a new book and returns its data with a status code confirming success.

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

  • A locally running REST API with an interactive Swagger UI in your browser for sending requests and seeing live JSON responses without installing any extra tools.
  • Full CRUD endpoints to create, retrieve, update, and delete books. Each endpoint returns the correct HTTP status code to confirm exactly what happened.
  • Data validation that rejects bad input with clear, structured error messages. Pagination and filtering that let your API handle large book collections the way a production service would.
  • Secret Mission: an optional challenge to push your skills further.

Are there any prerequisites?

No prior API experience is needed. This project walks you through every step, including installing FastAPI and setting up your development environment.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up FastAPI

To build your Bookstore API, you need a framework that handles routing requests, validating data, and generating documentation. FastAPI is a Python framework that does all of this out of the box.

In this step, you'll create a project folder, install FastAPI, and get your first endpoint running. By the end, you'll have a live development server and an interactive browser interface for testing your API.

In this step, get ready to:
  • Create a project folder and set up a Python virtual environment.
  • Install FastAPI and create your first endpoint.
  • Run the development server and open FastAPI's auto-generated Swagger UI.
Create a project folder and install FastAPI
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookstore-api
cd bookstore-api
  • Now open this folder in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Select the bookstore-api folder from your Desktop, then click Open.

VS Code's Explorer sidebar now shows the empty bookstore-api folder as your workspace.

Before installing any packages, you need an isolated Python environment for this project. A virtual environment keeps your bookstore packages separate from your system Python.

  • Open a fresh terminal by pressing Ctrl+` (backtick). This terminal starts inside your bookstore-api folder.
  • Create and activate a virtual environment by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

Your terminal prompt now starts with (venv). This confirms the virtual environment is active.

What is a 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 don't interfere with each other.

Not seeing (venv) in your prompt?

  • Make sure you ran both commands (create and activate) in the same terminal.
  • On Windows, if you see an execution policy error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell and try the activate command again.
  • Help me fix my virtual environment
  • Install FastAPI with its standard dependencies by running:
pip install "fastapi[standard]"

What does this command install?

The [standard] extra installs FastAPI along with everything you need for development, including the Uvicorn server that powers fastapi dev.

You'll see pip downloading and installing packages. When it finishes, you'll see a "Successfully installed" message with a list of packages.

Seeing a pip error?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • If you see "command not found", try pip3 instead of pip.
  • Help me fix this pip error
Create your first endpoint and run the dev server

FastAPI needs at least one endpoint to start serving requests. Let's create a simple root endpoint that returns a welcome message.

  • In VS Code's Explorer sidebar on the left, hover over the bookstore-api folder name.
  • Click the New File icon (it looks like a page with a plus sign).
  • Type main.py and press Enter.
  • Add this code to main.py:
# Import the FastAPI framework
from fastapi import FastAPI

# Create the API application instance
app = FastAPI()


# Define a GET endpoint at the root URL
@app.get("/")
def read_root():
    return {"message": "Welcome to the Bookstore API"}

What does this code do?

  • FastAPI() creates your API application. All of your endpoints will be registered on this app object.
  • @app.get("/") is a decorator that tells FastAPI to handle GET requests to the root URL (/).
  • read_root() returns a Python dictionary. FastAPI automatically converts it to JSON for the response.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Start the FastAPI development server by running this command in your terminal:
fastapi dev main.py

What does this command do?

fastapi dev starts a development server with auto-reload enabled. Every time you save changes to your code, the server restarts automatically. It listens on http://127.0.0.1:8000 by default.

This command takes over your terminal. Leave it running for the rest of this project. To stop it later, press Ctrl+C.

You'll see output in the terminal showing the server URL and a message that the application startup is complete.

  • Open your browser and navigate to http://127.0.0.1:8000.

You should see the JSON response: {"message": "Welcome to the Bookstore API"}. Your endpoint is live.

Before you visit the next URL, what do you think an auto-generated API documentation page would look like?

  • Navigate to http://127.0.0.1:8000/docs in your browser.

This is Swagger UI. You should see an interactive page listing your GET / endpoint with a Try it out button.

What is Swagger UI?

FastAPI automatically generates interactive documentation for every endpoint in your API. You can expand any endpoint, click Try it out, and send real requests directly from the browser.

This means you don't need to install any separate tools to test your API. You'll use this page throughout the entire project.

Page not loading?

  • Make sure the fastapi dev main.py command is still running in your terminal. If it stopped, run it again.
  • Check you typed http://127.0.0.1:8000/docs exactly, including the /docs at the end.
  • Help me troubleshoot my dev server

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and the dev server is still running in your terminal.

ⓧ I'd like to double check the full code

Here is the complete main.py at this point:

from fastapi import FastAPI

app = FastAPI()


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

Your development environment is ready and your first endpoint is live. Next up, you'll define what a "book" looks like and create endpoints to add and retrieve books from your API.

Create Your First Book Endpoints

Your FastAPI server is running, but right now it only returns a welcome message. That is not very useful for a bookstore.

Every API needs to define the shape of its data and give clients a way to create and retrieve it. In this step, you will define what a "book" looks like and build endpoints that let you add books and look them up.

In this step, get ready to:
  • Define Pydantic models to represent book data.
  • Create POST and GET endpoints for books.
  • Test the new endpoints in Swagger UI.
Define a Book model and create the POST endpoint

Every API endpoint needs to know what data it accepts and returns. Pydantic models let you define that shape once, and FastAPI uses it to validate incoming requests automatically.

  • In main.py, find the import line at the top of the file:
from fastapi import FastAPI
  • Replace it with these two lines:
from fastapi import FastAPI, status
from pydantic import BaseModel
  • Below the app = FastAPI() line and above the @app.get("/") decorator, add the following model definitions and storage variables:
class BookCreate(BaseModel):
    # Fields a client sends when creating a book
    title: str
    author: str
    year: int
    genre: str | None = None


class Book(BookCreate):
    # Extends BookCreate with a server-generated ID
    id: int


# In-memory storage for books
books: list[Book] = []
next_book_id = 1

What does this code do?

BookCreate defines the fields a client must provide when adding a book: title, author, year, and an optional genre.

Book inherits everything from BookCreate and adds an id field. The server assigns this ID, so the client never sends it.

books is a plain Python list that acts as your in-memory database. next_book_id is a counter that assigns a unique ID to each new book.

  • Save main.py.
  • Check your terminal. The dev server auto-reloads, and you should see a line that says the application startup is complete with no errors.

Seeing an error in the terminal?

  • Make sure from pydantic import BaseModel is on its own line directly below the fastapi import.
  • Check that class BookCreate(BaseModel): appears after app = FastAPI() and before the @app.get("/") decorator.
  • help me debug a startup error in my FastAPI bookstore project

Now that the data models are ready, you need an endpoint that uses them to accept new books.

  • Below the read_root function, add the POST endpoint that creates a new book:
@app.post("/books/", response_model=Book, status_code=status.HTTP_201_CREATED)
def create_book(book: BookCreate):
    global next_book_id
    # Create a new Book with the next available ID
    new_book = Book(id=next_book_id, **book.model_dump())
    books.append(new_book)
    next_book_id += 1
    return new_book

What does this endpoint do?

  • @app.post("/books/") registers a POST endpoint at the /books/ path.
  • response_model=Book tells FastAPI to serialize the response as a Book (including the ID).
  • status_code=status.HTTP_201_CREATED returns a 201 status code instead of the default 200. This follows REST conventions where 201 means "a new resource was created".
  • book.model_dump() converts the Pydantic model into a dictionary, and ** unpacks it so each field becomes a keyword argument for the Book constructor.
  • Save main.py.
  • Switch to your browser and open Swagger UI at http://127.0.0.1:8000/docs. Refresh the page.
  • Click the green POST /books/ row to expand it.
  • Click Try it out.
  • Replace the example JSON in the request body with this:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925,
  "genre": "fiction"
}
  • Click Execute.

You should see a 201 response with the book data, including an id of 1 that the server assigned.

Not seeing the POST endpoint in Swagger UI?

  • Hard-refresh the page with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). Swagger UI caches the page.
  • Check that the @app.post decorator is placed after the read_root function, not inside it.
  • help me fix my POST /books/ endpoint not appearing in Swagger UI
Create the GET endpoints to list and retrieve books

You can add books now, but there is no way to read them back. You need two GET endpoints: one to list all books, and one to look up a single book by its ID.

  • Below the create_book function, add this endpoint to list all books:
@app.get("/books/", response_model=list[Book])
def list_books():
    # Return every book in the store
    return books

What does this endpoint do?

response_model=list[Book] tells FastAPI this endpoint returns a list of Book objects. FastAPI uses this to generate the correct JSON schema in the docs and validate the response shape.

  • Save main.py and refresh Swagger UI.
  • Expand GET /books/, click Try it out, then click Execute.

You should see a JSON list containing the book you created earlier.

Seeing an empty list?

The in-memory storage resets every time the dev server reloads. If you saved main.py after creating a book, the server restarted and your data was cleared. POST a new book again, then try GET.

Now add a second GET endpoint to retrieve a single book by its ID.

  • Below the list_books function, add this endpoint:
@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    # Search for a book matching the requested ID
    for book in books:
        if book.id == book_id:
            return book

What is a path parameter?

{book_id} in the URL is a path parameter. When a client requests /books/3, FastAPI extracts 3 and passes it to the function as book_id. The int type hint makes FastAPI validate that it is a number automatically.

  • Save main.py and refresh Swagger UI.
  • POST a new book first (the server reloaded and cleared your data).
  • Expand GET /books/{book_id}, click Try it out, enter 1 for book_id, and click Execute.

You should see the single book you just created, returned as a JSON object.

✔️ Awesome, I've got everything!

Great. Make sure you have saved main.py.

ⓧ I'd like to double check the full code

Your main.py is longer than a quick glance, so scroll through it and confirm these sections are present in order:

  • Two import lines: from fastapi import FastAPI, status and from pydantic import BaseModel.
  • app = FastAPI().
  • BookCreate class with title, author, year, genre fields.
  • Book class extending BookCreate with an id field.
  • books: list[Book] = [] and next_book_id = 1.
  • The original read_root function at GET /.
  • create_book function at POST /books/.
  • list_books function at GET /books/.
  • get_book function at GET /books/{book_id}.
Test your endpoints in Swagger UI

You have a working API with three book endpoints. Before moving on, run through a full cycle to make sure everything connects.

  • In Swagger UI, use POST /books/ to add a second book with different details (pick any title, author, year, and genre you like).
  • Use GET /books/ to confirm both books appear in the list.
  • Use GET /books/{book_id} with book_id of 2 to retrieve the second book.

Now try something different. What do you think happens if you request a book ID that does not exist?

  • Use GET /books/{book_id} with book_id of 999 and click Execute.

You get a 200 status code with a response body of null. That is confusing. A client has no idea whether the book exists or not, because the API just silently returns nothing.

Why does the API return null?

The get_book function loops through the books list looking for a match. When no book matches, the function ends without returning anything. Python treats that as returning None, and FastAPI sends that back as null with a 200 OK status.

This is a real problem. A well-designed API should return a 404 status code with a clear error message when a resource does not exist. You will fix this in the next step.

Your bookstore API can create and retrieve books. Next up, you will fix that silent null response and add the Update and Delete endpoints to complete the full CRUD pattern.

Add Update, Delete, and Proper Error Handling

In the last step, you built endpoints to create and retrieve books. Your API handles the "C" and "R" in CRUD.

Your API is also missing Update and Delete endpoints. Before adding those, let's take a closer look at how your current API handles a request for a book that doesn't exist.

In this step, get ready to:
  • Add error handling so your API returns clear 404 responses for missing books.
  • Create PUT and DELETE endpoints to complete the CRUD pattern.
  • Test the full CRUD cycle in Swagger UI.
Add error handling with HTTPException

Your get_book endpoint can look up a book by its ID. But what happens when a client asks for an ID that doesn't exist?

  • Switch back to Swagger UI at http://127.0.0.1:8000/docs.
  • Click on the GET /books/{book_id} endpoint to expand it.
  • Click Try it out.
  • Enter 999 in the book_id field.

Before you click Execute, what do you think will happen? Will you get a clear error message telling you book 999 doesn't exist?

  • Click Execute.

You see a 500 Internal Server Error. The response body says {"detail": "Internal Server Error"}.

Why is this a problem?

Your get_book function loops through the books list. If no book matches the ID, the function ends without returning anything.

FastAPI expects your function to return a valid Book object (because of the response_model=Book setting). When it gets nothing back, it crashes with a 500 error. A client seeing this has no idea whether the server broke or the book simply doesn't exist.

Let's fix this by telling FastAPI exactly what to do when a book isn't found.

  • In main.py, find the import line at the top of the file:
from fastapi import FastAPI, status
  • Add HTTPException between FastAPI and status, so the line looks like this:
from fastapi import FastAPI, HTTPException, status
  • Now scroll down to the get_book function at the bottom of your file. It currently looks like this:
@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
  • Add the raise HTTPException line after the for loop, at the same indentation level as for. The function should now look like this:
@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")

What does HTTPException do?

HTTPException immediately stops the request and sends back a JSON response with the status code and message you provide.

Here, status_code=404 tells the client "this resource was not found." The detail field provides a human-readable explanation. The client receives {"detail": "Book not found"} instead of a cryptic server error.

  • Save main.py.

You should see the dev server reload automatically in your terminal. Now let's test the fix.

  • Switch back to Swagger UI at /docs.
  • Expand GET /books/{book_id} again, click Try it out, enter 999, and click Execute.

This time you see a 404 status code with {"detail": "Book not found"} in the response body. That's exactly the clear, structured error message a client needs.

Still seeing a 500 error?

  • Make sure you saved main.py. The dev server only reloads when the file is saved.
  • Check that HTTPException is in your import line. A missing import causes a NameError that also shows as a 500.
  • Verify the raise line is at the same indentation as for (inside the function, but outside the loop).

Help me debug my HTTPException error handling in FastAPI.

Add PUT and DELETE endpoints with proper status codes

Your API can create, read, and now properly reject bad requests. The two missing CRUD operations are Update (PUT) and Delete (DELETE).

Server auto-reload clears your data

Every time you save main.py, the dev server restarts and your in-memory books list resets to empty. You'll need to POST new books after each save before you can test PUT or DELETE.

  • In main.py, add the PUT endpoint below the get_book function by pasting this code:
# Update a book's data by ID, or return 404 if 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:
            # Replace the old book with updated fields, keeping the same 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?

  • enumerate(books) gives both the position (index) and the book object, so you can replace the book in the list.
  • book_data.model_dump() converts the incoming request body into a dictionary. The function creates a new Book with the same ID but updated fields.
  • If no book matches the ID, the same raise HTTPException pattern returns a 404.
  • Now add the DELETE endpoint directly below update_book by pasting this code:
# Remove a book by ID, return 204 No Content on success
@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_book(book_id: int):
    for index, book in enumerate(books):
        if book.id == book_id:
            books.pop(index)
            return
    raise HTTPException(status_code=404, detail="Book not found")

Why 204 No Content?

HTTP status code 204 means "the request succeeded, but there's nothing to send back." This is the standard response for a successful DELETE. The book is gone, so there's no data to return.

  • Save main.py.

The server reloads and your books list is cleared. Let's add a book and test both new endpoints.

  • In Swagger UI, use POST /books/ to create a book. Use any title, author, year, and genre you like.
  • Note the id in the response (it will be 1).
  • Expand PUT /books/{book_id}, click Try it out, and enter 1 for the book_id.
  • In the request body, change the title to something different from what you originally entered.
  • Click Execute.

You see a 200 response with the updated book data. The title reflects your change, and the id stayed the same.

  • Now expand DELETE /books/{book_id}, click Try it out, enter 1 for the book_id, and click Execute.

You see a 204 response with no body. The book has been removed.

Seeing a 405 Method Not Allowed?

  • Make sure you saved main.py after adding both new endpoints. If the server hasn't reloaded, the new routes don't exist yet.
  • Refresh the Swagger UI page in your browser. New endpoints only appear after a page reload.

Help me troubleshoot my PUT and DELETE endpoints in FastAPI.

Test the full CRUD cycle

You now have all four CRUD operations. Let's prove they work together by running through a complete lifecycle: create a book, read it, update it, and delete it.

  • In Swagger UI, use POST /books/ to create a new book. Note the id from the 201 response.
  • Use GET /books/{book_id} with that ID to retrieve it. Confirm the data matches what you submitted.
  • Use PUT /books/{book_id} with the same ID and change the title to something new.
  • Use GET /books/{book_id} again and confirm the title changed.
  • Use DELETE /books/{book_id} to remove the book. You should see a 204 response.

Now try GET /books/{book_id} one more time with the same ID. What do you expect to see?

  • Click Execute.

You get a 404 response with {"detail": "Book not found"}. The book is gone, and your API tells the client exactly what happened.

What just happened?

You proved all four CRUD operations work together. Each one returns the right HTTP status code: 201 for created, 200 for read and update, 204 for delete, and 404 for not found.

These status codes are how REST APIs communicate outcomes to clients. A well-designed API uses the right code for each situation so clients can handle responses without guessing.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py.

ⓧ I'd like to double check the full code

Your main.py is now over 30 lines, so here's a quick checklist. Scroll through your file and confirm each section is present, in this order:

  • Import line: from fastapi import FastAPI, HTTPException, status.
  • Pydantic import: from pydantic import BaseModel.
  • app = FastAPI() followed by BookCreate and Book models.
  • books list and next_book_id counter.
  • GET / root endpoint (read_root).
  • POST /books/ endpoint (create_book) with status 201.
  • GET /books/ endpoint (list_books).
  • GET /books/{book_id} endpoint (get_book) with raise HTTPException for 404.
  • PUT /books/{book_id} endpoint (update_book) with raise HTTPException for 404.
  • DELETE /books/{book_id} endpoint (delete_book) with status 204 and raise HTTPException for 404.

Your API now handles Create, Read, Update, and Delete with proper error messages for every failure case. Next up, you'll tighten the front door by adding data validation so clients can't sneak in bad data like a blank title or a negative year.

Add Validation and Structured Errors

Your API handles the full CRUD cycle and returns clear 404 errors for missing books. Every operation responds with the right status code.

But what about the data coming in? Right now, nothing stops a client from creating a book with a year of -5000 or a completely blank title.

In this step, get ready to:
  • Add Pydantic Field constraints to validate incoming book data.
  • Test that invalid data is rejected with structured error messages.
Add Pydantic Field validation to your models

Before adding any validation, test what your API does with bad data right now.

  • Switch back to the Swagger UI at http://127.0.0.1:8000/docs.
  • Expand the POST /books/ endpoint and click Try it out.

What do you think will happen if you submit a book with an empty title and a year of zero? Predict the response before you try it.

  • Replace the request body with this JSON:
{
  "title": "",
  "author": "",
  "year": 0,
  "genre": "test"
}
  • Click Execute.

The API returns 201 Created and happily stores a book with no title, no author, and a year of zero. Your API accepted completely invalid data without a single complaint.

This is the gap. Your BookCreate model defines field types but has no constraints on their values. Time to fix that.

  • In main.py, find the import line near the top of the file:
from pydantic import BaseModel
  • Add Field to the import so the line looks like this:
from pydantic import BaseModel, Field
  • Next, find the BookCreate class just below the imports:
class BookCreate(BaseModel):
    title: str
    author: str
    year: int
    genre: str | None = None
  • Update the first three fields to use Field constraints so the class looks like this:
class BookCreate(BaseModel):
    title: str = Field(min_length=1)
    author: str = Field(min_length=1)
    year: int = Field(gt=0)
    genre: str | None = None

What do these constraints do?

Field(min_length=1) means the string must be at least one character long. An empty string gets rejected.

Field(gt=0) means the integer must be greater than zero. A year of 0 or -5000 will not pass validation.

genre stays optional with no constraint. A book can have a genre or not.

  • Save main.py. The dev server auto-reloads your changes.

✔️ Awesome, I've got everything!

Great. Make sure you have saved main.py before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm these two edits are in place:

  • The import line reads from pydantic import BaseModel, Field.
  • BookCreate has Field(min_length=1) on title and author, and Field(gt=0) on year.
  • genre is unchanged: str | None = None.
Test validation in Swagger UI

Your model now has constraints. Time to send the same bad data and see what FastAPI does with it.

  • Return to the Swagger UI at http://127.0.0.1:8000/docs.
  • Expand the POST /books/ endpoint and click Try it out.

What do you think happens now when you submit the same invalid book? Predict the status code before you try it.

  • Enter this JSON in the request body:
{
  "title": "",
  "author": "",
  "year": 0,
  "genre": "test"
}
  • Click Execute.

This time, the API returns 422 Unprocessable Entity. FastAPI automatically caught the invalid data and rejected it with a structured error response.

What is a 422 response?

A 422 Unprocessable Entity status code means the server understood the request format, but the data inside failed validation.

FastAPI returns a JSON body with a detail array. Each entry tells the client exactly which field failed, where it is in the request (loc), what went wrong (msg), and the rejected value (input).

Now confirm that valid data still works.

  • Enter this JSON in the request body:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925,
  "genre": "fiction"
}
  • Click Execute.

The API returns 201 Created with the full book data. Valid data passes through. Invalid data gets rejected with clear error messages.

Still seeing 201 for invalid data?

  • Make sure you saved main.py after adding the Field constraints.
  • Check that the dev server reloaded. Look for a reload message in your terminal.
  • Confirm Field is imported on the same line as BaseModel: from pydantic import BaseModel, Field.

Why is my FastAPI validation not working after adding Field constraints?

Your API now validates every incoming book and rejects bad data with structured error messages. Next up, you will add pagination and filtering so the API handles large collections of books.

Add Filtering and Pagination

Your API now validates incoming data and rejects bad input with clear error messages. There is one more gap to close before your Bookstore API is production-ready.

Your GET /books/ endpoint dumps every single book in one response. Real APIs let clients request a page at a time and filter by criteria like genre or title.

In this step, get ready to:
  • Add pagination with skip and limit query parameters.
  • Add filtering by genre and search by title.
  • Test pagination, filtering, and combined queries in Swagger UI.
Add pagination with Query parameters

Query parameters are key-value pairs appended to a URL after a ? symbol. They let clients customize a request without changing the endpoint itself.

For pagination, you will add two query parameters: skip (how many books to skip over) and limit (the maximum number of books to return).

  • In main.py, find the import line at the top of the file:
from fastapi import FastAPI, HTTPException, status
  • Add Query to the import, so the line looks like this:
from fastapi import FastAPI, HTTPException, Query, status
  • Now scroll down to your list_books function and replace it with this updated version that adds pagination:
@app.get("/books/", response_model=list[Book])
def list_books(
    # How many books to skip (default: start from the beginning)
    skip: int = Query(0, ge=0),
    # Max books to return (default: 10, capped at 100)
    limit: int = Query(10, ge=1, le=100),
):
    # Return only the requested slice of books
    return books[skip : skip + limit]

What does this code do?

  • Query(0, ge=0) sets a default value of 0 for skip and enforces that it must be greater than or equal to 0.
  • Query(10, ge=1, le=100) sets a default page size of 10 and caps the maximum at 100. This prevents a client from requesting the entire database in one call.
  • books[skip : skip + limit] uses Python's list slicing to return only the requested window of results.
  • Save main.py.

Where did my books go?

When you save main.py, the FastAPI dev server auto-reloads, which restarts the Python process. Because your books live in an in-memory list, they are cleared on every reload. You will need to re-add books before testing.

  • Open Swagger UI at http://127.0.0.1:8000/docs in your browser.
  • Use the POST /books/ endpoint to add three books. For each book, click **Try it out**, fill in the JSON body, and click **Execute**.

Here is some example data you can use for each book:

{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925,
  "genre": "fiction"
}
{
  "title": "A Brief History of Time",
  "author": "Stephen Hawking",
  "year": 1988,
  "genre": "science"
}
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965,
  "genre": "fiction"
}
  • Now expand the GET /books/ endpoint, click Try it out, set skip to 0 and limit to 2, then click Execute.

You should see only 2 books in the response, even though you added 3. Pagination is working.

Still seeing all 3 books?

  • Make sure you saved main.py. The dev server only reloads on save.
  • Check that you replaced the entire list_books function, including its return line. If the old return books is still there, slicing will not apply.
  • Confirm that Query is imported from fastapi in your import line at the top.

Help me debug my pagination query parameters.

Add filtering by genre and search by title

Pagination controls how many results come back. Filtering controls which results come back. You will add two optional filters: an exact genre match and a title search.

  • In main.py, find your list_books function and replace it with this updated version that adds filtering:
@app.get("/books/", response_model=list[Book])
def list_books(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100),
    # Optional genre filter
    genre: str | None = None,
    # Optional title search
    search: str | None = None,
):
    # Start with all books
    results = books
    # Filter by genre if provided (case-insensitive)
    if genre is not None:
        results = [b for b in results if b.genre and b.genre.lower() == genre.lower()]
    # Filter by title search if provided (case-insensitive substring)
    if search is not None:
        results = [b for b in results if search.lower() in b.title.lower()]
    # Apply pagination after filtering
    return results[skip : skip + limit]

What do the new parts do?

  • genre: str | None = None and search: str | None = None are optional query parameters. When the client leaves them out, they default to None and no filtering happens.
  • The genre filter uses .lower() on both sides so that Fiction and fiction match. It also checks b.genre is not None before comparing, since genre is optional on the book model.
  • The search filter checks if the search term appears anywhere inside the book's title (substring match). Filtering happens first, then pagination slices the filtered results.
  • Save main.py.
  • Return to Swagger UI and refresh the page.

You should see the GET /books/ endpoint now has four query parameter fields: skip, limit, genre, and search.

Don't see the new parameters?

  • Hard-refresh the Swagger UI page with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). Swagger UI caches the API schema and a normal refresh may show stale parameters.
  • Check your terminal for errors. A syntax error in the function signature will prevent the server from reloading.

Help me debug my filtering query parameters in FastAPI.

Test with multiple books

Time to put your finished endpoint through its paces. You will add several books and test pagination, filtering, and both combined.

  • Use POST /books/ to add at least 5 books. Use a mix of genres (some fiction, some science, some history) so your filters have something to work with.

Test pagination first:

  • In GET /books/, set skip to 0 and limit to 2, then click Execute. You should see only the first 2 books.
  • Change skip to 2 and keep limit at 2, then click Execute. You should see the next 2 books.

Test filtering next:

  • Clear skip and limit back to their defaults. Type fiction in the genre field and click Execute. You should see only fiction books.
  • Clear genre. Type great in the search field and click Execute. You should see only books with "great" in the title.

Now for the combined test. Before you execute, what do you think will happen when you filter by fiction and set limit to 1?

  • Set genre to fiction, skip to 0, limit to 1, and clear search. Click Execute.

You should see exactly 1 fiction book. The API filtered the full list down to only fiction titles, then returned just the first page of 1 result. Filtering and pagination are working together.

✔️ Awesome, I've got everything!

Make sure you have saved main.py. Your Bookstore API is now feature-complete.

ⓧ I'd like to double check the full code

Your main.py is over 30 lines now, so rather than showing a wall of code here, scroll up and confirm each section is present in this order:

  • The import line with FastAPI, HTTPException, Query, and status from fastapi. The BaseModel and Field import from pydantic.
  • BookCreate model with title, author, year, and optional genre. The Book model extending it with an id field.
  • The in-memory books list and next_book_id counter.
  • read_root at GET /.
  • create_book at POST /books/ returning 201.
  • list_books at GET /books/ with skip, limit, genre, and search parameters, filtering logic, and pagination slicing.
  • get_book, update_book, and delete_book with HTTPException 404 handling.

Secret mission

Add Authors and Resource Relationships

Real APIs rarely have just one resource. Can you add a second resource for authors, link books to authors through a relationship, and query books by author?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine, 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 in your terminal).
  • The bookstore-api project folder on your Desktop (includes main.py, the venv virtual environment, and all installed packages).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your API.

  • Your dev server stays running and you can keep testing endpoints in Swagger UI.
  • To restart the server later, open a terminal in the bookstore-api folder, activate the virtual environment, and run fastapi dev main.py.

Pause - I'll come back to this later

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

  • Switch to the terminal where the FastAPI dev server is running.
  • Press Ctrl+C to stop the server.
  • When you are ready to come back, open a terminal in the bookstore-api folder, activate the virtual environment with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows), and run fastapi dev main.py.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your code, virtual environment, and all installed packages.

  • Switch to the terminal where the FastAPI dev server is running.
  • Press Ctrl+C to stop the server.
  • Deactivate the virtual environment by running:
deactivate
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/bookstore-api
  • On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\bookstore-api
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see bookstore-api in the listing.

Nice Work!

Nice Work!

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

You've learned how to:

  • Built a REST API with full CRUD endpoints (POST, GET, PUT, DELETE) that return proper HTTP status codes for every operation.
  • Used Pydantic models to define data schemas, validate incoming requests, and return structured error responses with HTTPException.
  • Added pagination and filtering with query parameters, and tested everything interactively through FastAPI's auto-generated Swagger UI.
  • Completed an optional Secret Mission to push your skills further.

Ready to quiz yourself?