Build a Bookshelf API with FastAPI

Build a REST API with full CRUD endpoints and auto-generated docs.

Introduction

30 Second Summary

Every time you check the weather on your phone, browse products on an online store, or scroll through social media, you're using an API behind the scenes. APIs are how apps talk to each other, and building one is one of the most valuable skills in software development.

In this project, you will build a REST API that manages a personal bookshelf. You'll use FastAPI, a modern Python framework that automatically validates your data and generates interactive documentation for testing.

What You'll Build

A personal bookshelf REST API with full CRUD operations, automatic data validation, and auto-generated interactive documentation.

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

  • A working FastAPI application with 5 REST endpoints for adding, browsing, updating, and deleting books.
  • Automatic request validation using Pydantic models that reject invalid data with clear error messages.
  • Auto-generated Swagger UI documentation for testing every endpoint live in the browser.
  • Secret Mission: A search endpoint with query parameter filtering to find books by title or author.

Are there any prerequisites?

You only need basic Python knowledge (variables, functions, dictionaries, and lists). No prior API or web development experience is required.

Set Up Your Development Environment

Before you can build an API, you need a few tools ready on your machine.

In this step, you'll verify that Python is installed, create an isolated project folder, and install FastAPI.

In this step, get ready to:
  • Verify Python 3.9+ is installed on your machine.
  • Create a project folder with a virtual environment.
  • Install FastAPI and confirm the CLI works.
Verify Python is installed

FastAPI requires Python 3.9 or higher to run. Let's confirm you have the right version before going further.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running the command for your operating system:

macOS

python3 --version

Windows

python --version

Check your result below:

✔️ I see version 3.9 or higher

You're all set. Move on to the next substep.

ⓧ I see an older version

You have Python installed, but it is below 3.9. You need to install a newer version.

  • Visit python.org/downloads and download Python 3.9 or higher.
  • Run the installer and follow the prompts.
  • On Windows, make sure to check the Add Python to PATH box during installation.
  • Close and reopen your terminal, then run the version check command again to confirm.

ⓧ Command not found

Python is not installed on your machine yet. Let's install it.

  • Visit python.org/downloads and download the latest Python 3 release.
  • Run the installer and follow the prompts.
  • On Windows, make sure to check the Add Python to PATH box during installation.
  • Close and reopen your terminal, then run the version check command again to confirm it works.
Create your project folder and virtual environment

A virtual environment is an isolated space where your project's packages live, separate from your system-wide Python installation. This keeps your projects from conflicting with each other.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookshelf-api
cd bookshelf-api
  • Confirm you're in the right place by running:
pwd

You should see a path ending in bookshelf-api.

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

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
.\venv\Scripts\Activate.ps1

Seeing a script execution error?

Windows PowerShell may block the activation script by default. Run this command to allow it, then try the activation command again:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

What does this do?

The first command creates a folder called venv containing a fresh copy of the Python interpreter and its own package directory.

The second command activates the environment so that any packages you install go into venv instead of your global Python installation.

  • Look at the beginning of your terminal prompt. You should see (venv) appear before your usual prompt text.

Don't see (venv) in your prompt?

Make sure you ran both commands in order. The first creates the environment, and the second activates it.

If you closed and reopened your terminal, you need to cd back into your bookshelf-api folder and run the activation command again.

help me activate my Python virtual environment

Install FastAPI

FastAPI is the Python framework you'll use to build your API endpoints. The [standard] extra installs everything you need including the development server.

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

What does this command do?

This installs the FastAPI framework along with the standard dependencies it needs to run a local development server.

Because your virtual environment is active, the package goes into the venv folder and won't affect anything else on your machine.

  • Verify the installation by running:
fastapi --version

You should see a version number printed in the output, confirming the FastAPI CLI is available.

Command not found after installing?

Make sure your virtual environment is still active. You should see (venv) at the start of your prompt.

If you accidentally closed the terminal, navigate back to your bookshelf-api folder, reactivate the environment, and run the install command again.

help me fix fastapi command not found after pip install

Your environment is ready. Next up, you'll create your first API endpoint and see it respond in the browser.

Create Your First FastAPI Endpoint

Your environment is ready to go. Now it's time to write actual code.

Every API starts with a single endpoint. An endpoint is a URL that your server responds to when someone visits it. You'll create one now, run it, and see the response live in your browser.

In this step, get ready to:
  • Create a main.py file with your first API endpoint.
  • Start the development server and see your API respond in the browser.
  • Explore the auto-generated interactive documentation.
Create your main.py file

Your API needs a home. You'll create a single Python file that defines your FastAPI application and its first endpoint.

  • Create a new file called main.py inside your bookshelf-api folder by running this command:
touch main.py
  • Confirm the file exists by running:
ls

You should see main.py and the venv folder in the output.

  • Open main.py in your code editor and add the following code:
# Import the FastAPI class
from fastapi import FastAPI

# Create the application instance
app = FastAPI()


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

What does this code do?

This file sets up your entire API in just a few lines.

  • from fastapi import FastAPI brings in the FastAPI class from the library you installed.
  • app = FastAPI() creates your application instance. This is the object that holds all your endpoints.
  • @app.get("/") is a decorator that tells FastAPI to call the function below it 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.

File not saving?

Make sure you're editing the main.py inside the bookshelf-api folder, not a different location. Press Cmd+S (macOS) or Ctrl+S (Windows) to save.

help me find where my file saved

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Welcome to the Bookshelf API"}
Start the development server

Your code is written, but it's not doing anything yet. You need to start a server that listens for incoming requests and runs your endpoint function when someone visits the URL.

  • Start the FastAPI development server by running this command in your terminal:
fastapi dev

What does this command do?

fastapi dev automatically finds your main.py file, detects the FastAPI app inside it, and starts a server using Uvicorn with auto-reload enabled.

Auto-reload means the server restarts itself every time you save a change to your code. No need to stop and start it manually.

You should see output including a line that says Uvicorn running on http://127.0.0.1:8000. This means your server is running.

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

You should see a JSON response in your browser that looks like this:

{"message":"Welcome to the Bookshelf API"}

Seeing an error or blank page?

  • Make sure your terminal still shows the server running. If you see an error, check that main.py is saved and has no typos.
  • Confirm your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • If the port is already in use, stop any other servers running on port 8000.

help me debug why my FastAPI server won't start

Explore the auto-generated Swagger UI documentation

FastAPI does something that most frameworks don't. It generates interactive documentation for your API automatically, based on the code you wrote.

  • In your browser, visit:
http://127.0.0.1:8000/docs

You should see a page titled FastAPI with your GET / endpoint listed. This is Swagger UI. It reads your code and builds a live testing interface from it.

  • Click the GET / row to expand it.
  • Click Try it out.
  • Click Execute.

You should see the response body with your welcome message, along with the response code 200 (which means success).

Why is auto-generated documentation useful?

Every endpoint you add to your code will appear here automatically. You can test any endpoint directly from the browser without needing a separate tool like Postman or curl.

This also makes it easy for other developers to understand your API. They can see every available endpoint, what data it expects, and what it returns.

Your first endpoint is live and documented. Next up, you'll define a data model for your books and build the endpoint that returns your entire collection.

Define Your Book Model with Pydantic

Your server is running and responding to requests. Now it's time to give your API real data to work with.

Every API needs a clear definition of the data it manages. In this step, you'll use Pydantic to define what a book looks like, load some sample data, and create an endpoint that serves your collection.

In this step, get ready to:
  • Define a Book model with Pydantic.
  • Add sample books to an in-memory data store.
  • Create a GET endpoint to serve your book collection.
Create a Pydantic Book model

Pydantic is a data validation library that comes bundled with FastAPI. You define a model class describing the shape of your data, and Pydantic automatically enforces those types at runtime.

  • In main.py, find the line that says from fastapi import FastAPI. Add this import directly below it:
from pydantic import BaseModel
  • Below the app = FastAPI() line, add the Book model by copying this code:
class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    status: str = "to-read"

What does this code do?

The from pydantic import BaseModel line brings in Pydantic's base class for data models.

The Book class defines five fields with type annotations. Each field enforces a type: id must be an integer, title must be a string, and so on. The status field has a default value of "to-read", so it becomes optional when creating a book.

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

Seeing an error in the terminal?

Make sure the from pydantic import BaseModel line is on its own line directly below from fastapi import FastAPI. Both imports must be at the top of the file.

Check that the class definition uses a colon after Book(BaseModel) and that each field is indented with four spaces.

help me fix a Pydantic model syntax error in my FastAPI app

Add sample books and a GET endpoint

Your model defines the shape of a book. Now you need some actual book data and an endpoint that serves it.

  • Below the Book class (after a blank line), add this list of sample books:
books = [
    {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "status": "read"},
    {"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960, "status": "reading"},
    {"id": 3, "title": "1984", "author": "George Orwell", "year": 1949, "status": "to-read"},
]

What does this code do?

The books list acts as an in-memory data store. Each dictionary represents a book with fields matching the Book model structure.

This data lives in your server's memory. It resets every time the server restarts. A real production API would use a database, but an in-memory list keeps things simple while you learn routing and validation.

You won't see a change yet. The next code block creates the endpoint that serves this data.

  • Below the existing read_root function, add this new endpoint:
@app.get("/books")
def get_books():
    return books

What does this code do?

The @app.get("/books") decorator registers a new GET endpoint at the /books path. When someone visits this URL, FastAPI calls get_books() and returns the full list of books as JSON automatically.

  • Save main.py.
  • Visit http://127.0.0.1:8000/books in your browser. You should see a JSON array containing three book objects with titles like "The Great Gatsby" and "1984".

Seeing a "Not Found" response?

Make sure the @app.get("/books") decorator is directly above def get_books(): with no blank line between them.

Check that books is defined at the module level (not inside another function) and appears before the endpoint function.

help me debug my FastAPI /books endpoint returning Not Found

Explore your endpoint in Swagger UI

FastAPI generates interactive documentation for every endpoint you create. This is one of its biggest advantages over other frameworks.

  • Visit http://127.0.0.1:8000/docs in your browser.
  • Find the GET /books endpoint in the list.
  • Click on it to expand it, then click Try it out.
  • Click Execute.

You should see the full JSON response with all three sample books. Notice how each book's fields match the structure you defined in your Book model.

Why is Swagger UI useful?

Swagger UI lets you test every endpoint directly in the browser without needing a separate tool like Postman or curl. It reads your Python code and generates the documentation automatically.

As you add more endpoints in the next step, they'll all appear here. You'll also see how Pydantic models generate request body schemas when you create POST endpoints.

✔️ Awesome, I've got everything!

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

ⓧ 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
from pydantic import BaseModel

app = FastAPI()


class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    status: str = "to-read"


books = [
    {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "status": "read"},
    {"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960, "status": "reading"},
    {"id": 3, "title": "1984", "author": "George Orwell", "year": 1949, "status": "to-read"},
]


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


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

Your API now has a data model and an endpoint serving real data. Next up, you'll build the full set of CRUD operations so you can add, update, and delete books from your collection.

Build the Full CRUD Operations

Your /books endpoint returns the full collection, but a real API needs to handle individual items too.

In this step, you'll build the remaining CRUD operations (Create, Read one, Update, Delete) so your API can manage individual books. You'll also see how FastAPI automatically rejects invalid data before your code even runs.

In this step, get ready to:
  • Add a GET endpoint that retrieves a single book by its ID.
  • Add a POST endpoint that creates new books with automatic validation.
  • Add PUT and DELETE endpoints to update and remove books.
Add a GET endpoint for a single book

Right now, the only way to see your books is to fetch all of them at once. A path parameter lets you target a specific book by putting its ID directly in the URL.

  • In main.py, find the import line at the very top of the file:
from fastapi import FastAPI
  • Add HTTPException to the import so the line looks like this:
from fastapi import FastAPI, HTTPException

What is HTTPException?

HTTPException is FastAPI's way of sending error responses. When a book isn't found, you'll raise this exception to return a 404 status code with a helpful message.

  • Add the following endpoint below your get_books function:
@app.get("/books/{book_id}")
def get_book(book_id: int):
    # Loop through books to find the one with a matching ID
    for book in books:
        if book["id"] == book_id:
            return book
    # Return a 404 error if no book matches
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

{book_id} in the route path tells FastAPI to capture that part of the URL as a variable. Adding book_id: int to the function signature means FastAPI will automatically validate that the value is an integer.

The function loops through the books list looking for a match. If no book has that ID, it raises an HTTPException which sends back a JSON error response.

  • Save main.py.
  • Switch back to your browser and go to http://127.0.0.1:8000/docs.
  • Find the new GET /books/{book_id} endpoint in the list, click it, then click Try it out.
  • Enter 1 for the book_id field and click Execute. You should see The Great Gatsby returned as JSON.
  • Try entering 99 and click Execute again. You should see a 404 response with the message Book not found.

Don't see the new endpoint in Swagger UI?

Make sure you saved main.py. The fastapi dev server auto-reloads on save, but unsaved changes won't appear.

If the page looks the same after saving, do a hard refresh in your browser (Cmd+Shift+R on macOS, Ctrl+Shift+R on Windows).

Help me debug why my GET /books/{book_id} endpoint isn't showing up in Swagger UI.

Add a POST endpoint to create books

Reading books is useful, but you also need a way to add new ones. A POST endpoint accepts data in the request body and adds it to your collection.

  • Add this endpoint below your get_book function in main.py:
@app.post("/books", status_code=201)
def create_book(book: Book):
    # Convert the Pydantic model to a dictionary and add to the list
    books.append(book.model_dump())
    return book

What does this code do?

status_code=201 tells FastAPI to return a 201 Created status instead of the default 200. This follows REST conventions for resource creation.

The book: Book parameter tells FastAPI to expect a JSON body matching your Book model. Pydantic validates every field automatically before your function runs.

.model_dump() converts the Pydantic model back into a plain dictionary so it matches the format of your existing books list.

  • Save main.py and return to Swagger UI at http://127.0.0.1:8000/docs.
  • Find the new POST /books endpoint, click it, then click Try it out.
  • Replace the example JSON body with this and click Execute:
{
  "id": 4,
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965,
  "status": "to-read"
}
  • You should see a 201 response with your new book returned as JSON.
  • Now test Pydantic's validation by sending invalid data. Click Try it out again and replace the body with this (missing the required title field):
{
  "id": 5,
  "author": "Unknown",
  "year": 2020
}
  • Click Execute. You should see a 422 Unprocessable Entity response with a detail message telling you the title field is required.

Why is automatic validation powerful?

You never wrote any validation code yourself. Pydantic checked the incoming data against your Book model and rejected the request before create_book even ran.

In a real application, this prevents bad data from ever reaching your database.

Getting a 500 error instead of 201?

Check that your function uses book.model_dump() (not book.dict() which is deprecated in Pydantic v2).

Also make sure the JSON you entered in Swagger has all required fields: id, title, author, and year.

Help me debug why my POST /books endpoint is returning a 500 error.

Add PUT and DELETE endpoints

The last two operations let you modify existing books and remove ones you no longer want. Both use the same path parameter pattern you already built.

  • Add the PUT endpoint below your create_book function in main.py:
@app.put("/books/{book_id}")
def update_book(book_id: int, updated_book: Book):
    # Find the book by ID and replace it with the updated version
    for index, book in enumerate(books):
        if book["id"] == book_id:
            books[index] = updated_book.model_dump()
            return updated_book
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

This endpoint takes both a path parameter (book_id) and a request body (updated_book). FastAPI knows the difference because book_id appears in the URL path while updated_book is a Pydantic model (so it comes from the body).

enumerate() gives you both the index position and the item, so you can replace the old book in the list with the new data.

  • Save main.py and return to Swagger UI.
  • Find the PUT /books/{book_id} endpoint, click Try it out, enter 1 for book_id, and update the JSON body to change the status to reading.
  • Click Execute. You should see the updated book returned in the response.

Seeing a 404 when updating?

Make sure the book_id value you enter matches an existing book's ID (1, 2, or 3 from the sample data).

Also check that the JSON body includes all required fields, not just the one you want to change. PUT replaces the entire book record.

Help me debug why my PUT /books/{book_id} endpoint is returning 404.

  • Now add the DELETE endpoint below your update_book function:
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    # Find the book by ID and remove it from the list
    for index, book in enumerate(books):
        if book["id"] == book_id:
            deleted = books.pop(index)
            return {"message": "Book deleted", "book": deleted}
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

books.pop(index) removes the book at that position and returns it. The endpoint sends back a confirmation message along with the deleted book's data so the caller knows exactly what was removed.

  • Save main.py and return to Swagger UI.
  • Find the DELETE /books/{book_id} endpoint, click Try it out, enter 3 for book_id, and click Execute.
  • You should see a response with the message Book deleted and the removed book's data.
  • Go back to the GET /books endpoint and execute it. You should see only 2 books remaining (plus any you added with POST).

Deleted book still showing up?

Make sure you're testing GET /books (all books) and not the GET endpoint for the specific ID you just deleted.

Remember that the in-memory list resets every time the server restarts. If you saw an auto-reload in the terminal, your sample data was restored.

Help me understand why my deleted book is still appearing in the list.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and all five endpoints are working in Swagger UI.

ⓧ I'd like to double check the full code

Here's the complete main.py file at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    status: str = "to-read"


books = [
    {"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925, "status": "read"},
    {"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee", "year": 1960, "status": "reading"},
    {"id": 3, "title": "1984", "author": "George Orwell", "year": 1949, "status": "to-read"},
]


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


@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
    raise HTTPException(status_code=404, detail="Book not found")


@app.post("/books", status_code=201)
def create_book(book: Book):
    books.append(book.model_dump())
    return book


@app.put("/books/{book_id}")
def update_book(book_id: int, updated_book: Book):
    for index, book in enumerate(books):
        if book["id"] == book_id:
            books[index] = updated_book.model_dump()
            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:
            deleted = books.pop(index)
            return {"message": "Book deleted", "book": deleted}
    raise HTTPException(status_code=404, detail="Book not found")

Secret mission

Add Search with Query Parameters

Your bookshelf API can list all books and fetch one by ID. But what if you have hundreds of books and want to find one by a specific author or title without scrolling through the entire collection? Add a search endpoint that filters your bookshelf using optional query parameters.

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 (currently running in your terminal).
  • The venv virtual environment inside your project folder.
  • The bookshelf-api project folder on your Desktop.

Keep everything running

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

  • Your FastAPI dev server stays running and accessible at http://127.0.0.1:8000.
  • Your virtual environment stays activated in the terminal.
  • Your bookshelf-api folder remains on your Desktop.

Pause - I'll come back to this later

Shut down running processes to free up memory, but keep your project files for later.

  • In your terminal, press Ctrl+C to stop the FastAPI dev server.
  • Deactivate your virtual environment by running:
deactivate

When you're ready to resume, navigate back into your bookshelf-api folder, reactivate the virtual environment with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows), and run fastapi dev.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • In your terminal, press Ctrl+C to stop the FastAPI dev server.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate up one level and delete the entire project folder by running:

macOS

cd ~/Desktop
rm -rf bookshelf-api

Windows

cd ~\Desktop
rmdir /s /q bookshelf-api
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see bookshelf-api in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional REST API from scratch using FastAPI and tested every endpoint through interactive documentation in your browser.

You've learned how to:

  • Built a FastAPI application with 5 working CRUD endpoints that create, read, update, and delete books.
  • Used Pydantic models for automatic request and response validation that rejects invalid data with clear error messages.
  • Explored auto-generated Swagger UI interactive documentation to test every endpoint live in the browser.
  • Added search functionality with optional query parameter filtering for case-insensitive partial matching.

Ready to quiz yourself?