Build a Book Collection REST API

Build a CRUD REST API with FastAPI, Pydantic validation, and interactive docs.

Introduction

30 Second Summary

Think about how you keep track of books you've read or want to read. Maybe it's a spreadsheet, a notes app, or just a mental list that's always incomplete.

In this project, you will build a REST API that manages a personal book collection using FastAPI and Python. You'll create endpoints to add, view, update, and delete books, then test everything through interactive documentation that FastAPI generates for you automatically.

What You'll Build

You'll open the auto-generated Swagger UI in your browser and add, update, and delete books right there without needing a frontend or any extra tools.

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

  • A running API that returns your full book collection as structured JSON and lets you look up any single book by its ID.
  • An endpoint where you add a new book by sending its title, author, and year. FastAPI automatically rejects invalid data with a helpful error message.
  • Full CRUD operations with proper error handling that returns meaningful HTTP status codes. You can test every endpoint in your browser through the interactive docs at /docs.
  • Secret Mission: Add search and filtering so you can find books by author or reading status.

Do I need to know Python?

This project assumes you're comfortable with Python basics like writing functions, using classes, and adding type hints. You don't need any web development experience.

Before We Start

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

Set Up FastAPI and Run Your First Server

To build your book collection API, you need the FastAPI framework installed and a local development server running.

Getting the server running first means you'll see instant feedback as you add features in the steps ahead. Every time you save a change, the server reloads automatically.

In this step, get ready to:
  • Create a project folder and Python virtual environment.
  • Install FastAPI and its development dependencies.
  • Run your first FastAPI server and explore the auto-generated API docs.
Create a project directory and virtual environment

A virtual environment keeps your project's packages isolated from other Python projects on your machine. You'll create one inside a new project folder on your Desktop.

  • 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+` (Windows) or Cmd+` (macOS).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir book-api
cd book-api
  • Confirm you're in the right place by running:
pwd

You should see a path ending in book-api.

  • Create and activate a Python virtual environment by running the commands for your operating system:

macOS

  • Create and activate the virtual environment by running:
python3 -m venv venv
source venv/bin/activate

Windows

  • Create and activate the virtual environment by running:
python -m venv venv
venv\Scripts\activate

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

What is a virtual environment?

A virtual environment is an isolated Python workspace. Packages you install here stay separate from your system Python, so different projects can use different package versions without conflicting.

  • Install FastAPI and its development tools by running:
pip install "fastapi[standard]"

What does this command do?

The [standard] extra installs FastAPI along with everything you need for development. This includes the web server (Uvicorn) and the fastapi CLI command you'll use to run your server.

You should see pip downloading and installing several packages. The final line will say Successfully installed followed by a list of package names.

Seeing a 'command not found' error?

If pip is not found, make sure your virtual environment is active. Your prompt should start with (venv). If it does not, re-run the activation command from the platform tab above.

Help me fix my pip install issue.

Create the starter app

FastAPI apps start with a single Python file that defines your server and its endpoints. You'll create a minimal app that responds to one REST API request.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the book-api folder, then click Open.
  • Create a new file called main.py by clicking the new file icon in the Explorer sidebar.
  • Copy the following code into main.py:
from fastapi import FastAPI

# Create the FastAPI application instance
app = FastAPI()


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

What does this code do?

  • from fastapi import FastAPI brings in the framework you just installed.
  • app = FastAPI() creates your application instance. All your endpoints attach to this object.
  • @app.get("/") is a decorator that registers the function below as a handler for GET requests to the root URL.
  • Returning a Python dictionary automatically converts it to a JSON response.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to running the server.

ⓧ I'd like to double check the full code

Here's what main.py should look like:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Hello World"}
Run the development server

FastAPI includes a built-in development server that watches your files and reloads automatically when you make changes. You'll start it now and see your app respond to its first request.

  • Open the integrated terminal again with Ctrl+` (Windows) or Cmd+` (macOS).
  • Make sure your terminal still shows (venv) at the start of the prompt. If it does not, re-run the activation command from earlier.
  • Start the development server by running:
fastapi dev main.py

What does this command do?

This starts a local Uvicorn server with auto-reload enabled. The server takes over your terminal, so you'll see log messages appear there as requests come in. You do not need to stop and restart it when you edit your code.

You should see output showing the server is running, including a URL. Before you open it, what do you think you'll see when you visit that URL in your browser?

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

You should see a JSON response: {"message":"Hello World"}. That is your FastAPI server responding to a GET request at the root URL.

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

You should see the interactive API documentation page (Swagger UI). FastAPI generates this automatically from your code.

  • Click on the GET / endpoint, then click Try it out.
  • Click Execute to test your endpoint directly from the browser.

You should see a response body of {"message":"Hello World"} with a 200 status code.

Server not starting?

  • If you see command not found: fastapi, your virtual environment is not active. Re-run the activation command and try again.
  • If you see an import error, make sure you ran pip install "fastapi[standard]" inside the active virtual environment (with the (venv) prefix showing).
  • If the browser shows 'connection refused', confirm the terminal says the server is running and check the URL is exactly http://127.0.0.1:8000.

Help me troubleshoot my FastAPI dev server.

Your development environment is ready and your server is live. Next up, you'll add real book data and build endpoints that serve it as JSON.

Build Your Book List Endpoints

Your server is running and responding to requests. Now it needs to serve actual data instead of a generic hello message.

In this step, you'll create read-only endpoints that let anyone browse your book collection through the interactive docs. Along the way, you'll discover an important gap in how your API handles missing data.

In this step, get ready to:
  • Add hardcoded book data and a GET /books endpoint.
  • Add a GET /books/{book_id} endpoint for individual books.
  • Observe what happens when a requested book doesn't exist.
Add hardcoded book data

The hello world endpoint proved your server works. Now you'll replace it with a list of books that your API can serve to clients.

  • In main.py, find and delete the hello world endpoint (the @app.get("/") decorator and the read_root function below it).
  • Add this list of book data below the app = FastAPI() line:
books = [
    {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "read": True},
    {"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "read": False},
]

What does this code do?

books is a Python list of dictionaries. Each dictionary represents one book with an id, title, author, year, and read status.

This acts as your in-memory database. Any changes reset when you restart the server.

  • Add this endpoint below the books list:
@app.get("/books")
def get_books():
    return books

What does this code do?

@app.get("/books") tells FastAPI to call get_books() whenever a GET request hits /books.

The function returns the entire books list. FastAPI automatically converts Python dictionaries and lists into JSON for the response.

  • Save main.py.
  • Switch to http://127.0.0.1:8000/docs in your browser. You should see the GET /books endpoint listed.
  • Click GET /books, then click Try it out, then click Execute.
  • You should see the full list of two books in the response body.

Don't see GET /books in the docs?

  • Check that your file is saved. The dev server reloads automatically on save.
  • Look at the terminal where fastapi dev is running. If you see a syntax error, check for missing commas between the book dictionaries or missing closing brackets.
  • Make sure you saved the file as main.py in the same folder where you ran fastapi dev.

Why isn't my GET /books endpoint showing up in the docs?

Add a single-book endpoint

Listing all books works. Now you need a way to fetch one specific book by its ID.

  • Add this endpoint below the get_books function in main.py:
@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    return None

What does this code do?

{book_id} in the path is a path parameter. FastAPI extracts it from the URL and passes it to your function.

book_id: int tells FastAPI to validate that the value is an integer. If someone sends /books/abc, FastAPI rejects it with a 422 error automatically.

The function loops through books looking for a matching ID. If no match is found, it returns None.

  • Save main.py.
  • In http://127.0.0.1:8000/docs, you should now see a second endpoint: GET /books/{book_id}.
  • Click it, click Try it out, enter 1 in the book_id field, then click Execute.
  • You should see The Great Gatsby's data in the response body.

Now try a book that doesn't exist. Before you do, what do you think the response will look like if you request book ID 999? What status code would you expect a well-designed API to return?

  • Enter 999 in the book_id field and click Execute.

You get null in the response body with a 200 OK status code. That's misleading. A client has no way to tell whether the book has null data or simply doesn't exist. A proper REST API should return a 404 Not Found error instead. You'll fix this in the next step.

Not seeing the endpoint?

  • If the docs page is empty or shows an error, check the terminal for a traceback. A common cause is a missing colon after the function definition line.
  • If you see GET /books/{book_id} but get unexpected results, make sure you're comparing book["id"] (string key) to book_id (integer parameter).

Why isn't my single-book endpoint working correctly?

✔️ Awesome, I've got everything!

Great. Make sure you've saved main.py and you can see both endpoints working in the docs.

ⓧ 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()

books = [
    {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "read": True},
    {"id": 2, "title": "1984", "author": "George Orwell", "year": 1949, "read": False},
]


@app.get("/books")
def get_books():
    return books


@app.get("/books/{book_id}")
def get_book(book_id: int):
    for book in books:
        if book["id"] == book_id:
            return book
    return None

You've built two working endpoints and discovered a real gap in your API's error handling. Next up, you'll add the ability to create new books with automatic validation and fix that misleading null response with a proper 404 error.

Add Books with Validation and Fix Error Handling

In the previous step, you saw a problem. When you requested a book ID that doesn't exist, your API returned null with a 200 OK status. That's misleading. A 200 status tells the client everything went fine, when really the book wasn't found.

In this step, you'll fix that gap and add the ability to create new books. Pydantic models give you automatic input validation for free. HTTPException lets you return proper error responses that tell the client exactly what went wrong.

In this step, get ready to:
  • Define Pydantic models that validate book data automatically.
  • Add a POST endpoint that creates new books with a 201 status code.
  • Fix the GET endpoint to return a proper 404 error for missing books.
Create Pydantic models

Right now your book data is stored as plain Python dictionaries. Dictionaries don't enforce any structure. Someone could add a book missing a title, or set the year to a string, and Python wouldn't complain.

Pydantic models define the exact shape your data must follow. FastAPI uses these models to automatically validate incoming requests and reject bad data before your code even runs.

  • In main.py, replace everything from the import line down through the books list (but keep the two endpoint functions at the bottom) with the following code:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


# Define the shape of data needed to create a book
class BookCreate(BaseModel):
    title: str
    author: str
    year: int | None = None
    read: bool = False


# Full book includes an ID assigned by the server
class Book(BookCreate):
    id: int


# Store books as validated Book instances
books: list[Book] = [
    Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", year=1925, read=True),
    Book(id=2, title="1984", author="George Orwell", year=1949, read=False),
]
next_id: int = 3

What does this code do?

BookCreate defines the fields a client sends when adding a book. title and author are required strings. year is optional (can be None). read defaults to False.

Book inherits everything from BookCreate and adds an id field. The server assigns the ID, so clients never send one.

The books list now holds Book instances instead of raw dictionaries. next_id tracks the next available ID for new books.

Since the books are now Pydantic model instances (not dictionaries), you need to update how the get_book endpoint accesses the ID field.

  • In the get_book function, find this line:
        if book["id"] == book_id:
  • Replace book["id"] with book.id so the line looks like this:
        if book.id == book_id:
  • Save main.py.
  • Switch to your browser and open http://127.0.0.1:8000/docs. Click GET /books, then Try it out, then Execute.

You should see the same two books returned as JSON. The data structure is identical, but now it's backed by validated Pydantic models instead of loose dictionaries.

Seeing a server error?

Check that you changed book["id"] to book.id in the get_book function. Pydantic models use dot notation for attribute access, not dictionary bracket notation.

Also make sure you didn't accidentally delete the get_books and get_book endpoint functions when replacing the top of the file.

Help me debug my Pydantic model conversion in FastAPI

Add the POST endpoint with validation

Now that your data layer uses Pydantic models, adding a create endpoint is straightforward. FastAPI automatically reads the request body, validates it against your model, and rejects invalid data with a helpful error message.

  • In main.py, find the import line at the top:
from fastapi import FastAPI
  • Replace it with this line to add the status module:
from fastapi import FastAPI, status
  • Add the following POST endpoint at the bottom of main.py, below the existing get_book function:
@app.post("/books", status_code=status.HTTP_201_CREATED)
def create_book(book_data: BookCreate):
    # Use global counter to assign unique IDs
    global next_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?

The @app.post decorator creates an endpoint that responds to POST requests. The status_code=status.HTTP_201_CREATED tells FastAPI to return a 201 status instead of the default 200, signalling that a new resource was created.

The book_data: BookCreate parameter tells FastAPI to read the request body as JSON and validate it against the BookCreate model. If the data is invalid, FastAPI automatically returns a 422 error with details about what's wrong.

The model_dump() method converts the Pydantic model to a dictionary, and ** unpacks it as keyword arguments to create the full Book instance with an ID.

  • Save main.py.
  • In the browser at http://127.0.0.1:8000/docs, refresh the page. You should see a new POST /books endpoint in the list.
  • Click POST /books, then Try it out.
  • Replace the request body with this JSON:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965
}
  • Click Execute.

You should see a response with status code 201 and a body showing your new book with id: 3 and read: false (the default).

Now test the validation. FastAPI should reject data that doesn't match the model.

  • Click Try it out again on POST /books and replace the body with this invalid JSON:
{
  "title": 123
}
  • Click Execute.

You should see a 422 Unprocessable Entity response. FastAPI detected that title should be a string and author is missing. You didn't write any validation code yourself. Pydantic handled it.

POST endpoint not showing up?

Make sure you saved main.py after adding the new endpoint. The dev server auto-reloads on save. Refresh the /docs page if it hasn't updated.

Check that the @app.post decorator is at the same indentation level as the other decorators (not nested inside another function).

Help me fix my POST endpoint in FastAPI

Fix the GET endpoint with proper error handling

Remember when you tried GET /books/999 in the previous step and got null with a 200 OK? That's the bug you're fixing now. Instead of returning nothing, the API should tell the client the book doesn't exist with a 404 status code.

  • In main.py, find the import line at the top:
from fastapi import FastAPI, status
  • Replace it with this line to add HTTPException:
from fastapi import FastAPI, HTTPException, status
  • In the get_book function, find this line at the bottom:
    return None
  • Replace it with a proper 404 error:
    raise HTTPException(status_code=404, detail="Book not found")

What does HTTPException do?

HTTPException raises an HTTP error response that stops the function immediately. The status_code tells the client what went wrong (404 means "not found"). The detail string becomes the error message in the JSON response body.

This is much better than returning None because clients can distinguish between "found successfully" (200) and "doesn't exist" (404) by checking the status code.

  • Save main.py.

Before you test this, what do you think you'll see when you request a book ID that doesn't exist? What status code and message do you expect?

  • In the browser at http://127.0.0.1:8000/docs, click GET /books/{book_id}, then Try it out.
  • Enter 999 as the book_id value.
  • Click Execute.

You should see a 404 response with the body {"detail": "Book not found"}. No more confusing null with a 200. The client now knows exactly what happened.

Still seeing null with a 200?

Make sure you replaced return None with the raise HTTPException(...) line. It's raise, not return. Also check that the HTTPException import is at the top of the file.

Help me fix my 404 error handling in FastAPI

✔️ Awesome, I've got everything!

Great. Make sure you've saved main.py and confirmed all three features work: GET returns books, POST creates new ones with validation, and missing IDs return 404.

ⓧ I'd like to double check the full code

Scroll up and confirm your main.py contains each of these sections in order:

  • Imports: from fastapi import FastAPI, HTTPException, status and from pydantic import BaseModel.
  • The BookCreate class with four fields (title, author, year, read).
  • The Book class inheriting from BookCreate with an id field.
  • The books list with two Book instances and next_id: int = 3.
  • GET /books returning books.
  • GET /books/{book_id} using book.id and raising HTTPException(status_code=404, detail="Book not found").
  • POST /books with status_code=status.HTTP_201_CREATED and the create_book function.

Your API now validates incoming data, creates new books, and returns proper error codes. Next up, you'll complete the REST pattern by adding the ability to update and delete books.

Update and Delete Books

In Step 3, you added Pydantic validation and proper error handling. Your API now rejects bad data and returns clear 404 responses when a book does not exist.

Right now, your API can read and create books, but there is no way to change a book's details or remove one entirely. A complete REST API needs all four CRUD operations (Create, Read, Update, Delete). Update and delete follow the same find-or-404 pattern you already built, but use different HTTP methods and status codes.

In this step, get ready to:
  • Add a PUT endpoint to update existing books.
  • Add a DELETE endpoint to remove books from the collection.
Add the PUT endpoint

The PUT HTTP method means "replace this resource with new data." It follows the same find-or-404 pattern you built for GET, but accepts a request body containing the replacement data.

  • In main.py, add the following code below your create_book function:
@app.put("/books/{book_id}")
def update_book(book_id: int, book_data: BookCreate):
    # Loop through books to find the matching ID
    for index, book in enumerate(books):
        if book.id == book_id:
            # Replace with new data, keeping the same ID
            updated_book = Book(id=book_id, **book_data.model_dump())
            books[index] = updated_book
            return updated_book
    # No match found
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

  • enumerate(books) gives you both the position (index) and the book object as you loop. You need the index so you can swap the old book for the new one in the list.
  • Book(id=book_id, **book_data.model_dump()) creates a new Book instance with the original ID but all the new field values from the request body.
  • If no book matches the ID, the function raises a 404 error. This is the same pattern you used in get_book.
  • Save main.py. The dev server auto-reloads your changes.
  • Return to http://127.0.0.1:8000/docs in your browser and refresh the page.
  • Expand the new PUT /books/{book_id} section and click Try it out.
  • Enter 1 for the book_id field.
  • In the request body, change the title to something new. For example:
{
  "title": "The Great Gatsby (Updated Edition)",
  "author": "F. Scott Fitzgerald",
  "year": 1925,
  "read": true
}
  • Click Execute.

You should see a 200 response with the updated book data, including your new title and the same id: 1.

Not seeing the PUT endpoint in /docs?

  • Make sure you saved main.py. The dev server only reloads when the file is saved to disk.
  • Check your terminal for error messages. A syntax error (missing colon, wrong indentation) prevents the server from reloading.
  • Confirm the new function is placed after the create_book function and not nested inside it.

Why isn't my PUT endpoint showing up in the FastAPI docs?

Add the DELETE endpoint

The DELETE HTTP method removes a resource entirely. Unlike PUT, there is nothing to send back after deletion, so the API returns a 204 No Content status code.

  • In main.py, add the following code below your update_book function:
@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_book(book_id: int):
    # Loop through books to find the matching ID
    for index, book in enumerate(books):
        if book.id == book_id:
            # Remove the book from the list
            books.pop(index)
            return
    # No match found
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

  • status.HTTP_204_NO_CONTENT in the decorator tells FastAPI that this endpoint returns an empty body on success. The client knows the deletion worked purely from the status code.
  • books.pop(index) removes the book at that position from the list.
  • The bare return exits the function without returning a value, which matches the 204 "no content" promise.
  • Save main.py.
  • Return to http://127.0.0.1:8000/docs and refresh the page.
  • Expand DELETE /books/{book_id} and click Try it out.
  • Enter 1 for the book_id and click Execute.

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

Now, before you test this next part, predict what you think will happen if you try GET /books/1.

  • Expand GET /books/{book_id}, click Try it out, enter 1 for the book_id, and click Execute.

You get a 404 response with {"detail": "Book not found"}. The book no longer exists, and your error handling correctly communicates that to the client.

Still seeing the book after deleting?

  • Make sure you clicked Execute on the DELETE request first, not just the GET request.
  • Check that the book_id you deleted matches the one you are requesting with GET.
  • If you restarted the server, the in-memory data resets back to the original two books. Delete again after restarting.

Help me figure out why my DELETE endpoint is not removing the book.

Why does the data reset when the server restarts?

Your books list lives in Python's memory. Every time the server restarts (including when auto-reload kicks in after a file save), the list resets to the two hardcoded books. A real production API would store data in a database to persist it across restarts.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that all five endpoints are visible in your /docs page.

ⓧ I'd like to double check the full code

Your main.py should now contain these sections in order. Scroll up and confirm each one is present:

  • Imports (FastAPI, HTTPException, status, BaseModel).
  • The app = FastAPI() instance.
  • The BookCreate and Book Pydantic models.
  • The books list and next_id variable.
  • GET /books endpoint (get_books).
  • GET /books/{book_id} endpoint (get_book) with 404 handling.
  • POST /books endpoint (create_book) with 201 status.
  • PUT /books/{book_id} endpoint (update_book) with 404 handling.
  • DELETE /books/{book_id} endpoint (delete_book) with 204 status and 404 handling.

Your API now supports all four CRUD operations. You can create, read, update, and delete books, all testable through the interactive docs. Next up, you will push this further with a Secret Mission that adds search and filtering.

Secret mission

Add Search and Filtering

Your API returns the entire book list every time someone calls GET /books. Real-world APIs let clients filter results with query parameters so they get exactly the data they need. Can you add author and read-status filters to your existing endpoint?

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 development server (a running process in your terminal).
  • Your project folder on the Desktop (contains main.py, your virtual environment, and the installed FastAPI package).

Keep everything running

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

  • Your development server continues running at http://127.0.0.1:8000.
  • Your project folder and virtual environment remain on your Desktop.
  • You can keep adding endpoints or modifying your API at any time.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your files stay on your Desktop for when you return.

  • Switch to the terminal where your FastAPI server is running.
  • Press Ctrl+C to stop the development server.
  • When you return, reactivate your virtual environment and run fastapi dev main.py to start the server again.

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 your FastAPI server is running.
  • Press Ctrl+C to stop the development server.
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
  • Confirm the folder is gone by running:
ls

Your project folder should no longer appear in the output.

Nice Work!

Nice Work!

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

You've learned how to:

  • Build a REST API server from scratch and implement all four CRUD operations (Create, Read, Update, Delete) using the correct HTTP methods and status codes.
  • Use Pydantic models for automatic request validation and proper error handling with HTTPException to return meaningful error messages and status codes.
  • Test and explore every endpoint through FastAPI's auto-generated interactive documentation without needing any external tools.
  • Secret Mission: Added query parameter filtering to search books by author and reading status.

Ready to quiz yourself?