Build a Bookshelf REST API with FastAPI

Build a CRUD REST API with auto-generated Swagger docs using FastAPI.

Introduction

30 Second Summary

Every app you use daily talks to a server through an API. When you search for a product, save a playlist, or post a comment, an API handles that request behind the scenes.

In this project, you will build a REST API from scratch using Python and FastAPI. You'll create endpoints that let users add, browse, update, and delete books from a personal collection.

What You'll Build

A fully functional REST API that manages a personal book collection, with auto-generated interactive documentation you can test directly in your browser.

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

  • A working REST API with full CRUD operations (Create, Read, Update, Delete) running locally.
  • Auto-generated interactive Swagger UI documentation you can demo in your portfolio.
  • Hands-on experience with Pydantic data validation and proper HTTP status codes.
  • Secret Mission: Add filtering and search to your book collection API.

Are there any prerequisites?

You should be comfortable with basic Python concepts like functions, dictionaries, and type hints. No web development or API experience is needed.

Set Up Your Project

Every Python project benefits from a clean, isolated environment. A virtual environment keeps your project's packages separate from your system Python, preventing version conflicts down the road.

In this step, you'll verify your Python installation, create a dedicated project folder on your Desktop, and install FastAPI inside an isolated virtual environment.

In this step, get ready to:
  • Verify Python 3.10+ is installed.
  • Create a project directory with a virtual environment.
  • Install FastAPI with its standard dependencies.
Open your terminal and verify Python

FastAPI requires Python 3.10 or higher. Before creating any files, confirm you have the right version installed.

  • In VS Code, click Terminal in the top menu bar.
  • Click New Terminal.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.10 or higher

You're all set. Your Python version is compatible with FastAPI. Continue to the next substep below.

ⓧ I see an older version

Your system has an older Python installed, but you need 3.10+ for this project.

On macOS, the system Python is often outdated. Install a newer version alongside it using Homebrew:

  • Install the latest Python by running:
brew install python3
  • On Windows, download the latest installer from python.org/downloads.
  • Run the installer and check the box that says Add python.exe to PATH.
  • After installing, close and reopen your terminal, then run python3 --version again to confirm.

ⓧ Command not found

Python is not installed on your system yet.

  • On macOS, install Python using Homebrew by running:
brew install python3
  • On Windows, download the installer from python.org/downloads.
  • Run the installer and check the box that says Add python.exe to PATH.
  • After installing, close and reopen your terminal, then run python3 --version to confirm.
Create your project directory and virtual environment

A virtual environment is an isolated Python installation that lives inside your project folder. Packages you install here won't affect your system Python or other projects.

  • 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
  • Create and activate your virtual environment by running the commands for your operating system:

macOS

python3 -m venv venv
source venv/bin/activate

Linux users can run the same commands above.

Windows

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

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

What is a virtual environment?

Think of it as a sealed box for your project's packages. When the virtual environment is active, any package you install with pip goes into the venv folder instead of your system-wide Python.

This prevents version conflicts between projects. One project can use FastAPI 0.136 while another uses a different version, with no interference.

Don't see (venv) in your prompt?

On Windows, PowerShell may block the activation script due to its execution policy. Run this command first, then try activating again:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

On macOS/Linux, make sure you typed source before the path. Without it, the script runs in a subshell and the activation won't persist.

Help me activate my Python virtual environment.

Install FastAPI

With your virtual environment active, you can now install FastAPI and its standard dependencies.

  • Install FastAPI with standard dependencies by running this command:
pip install "fastapi[standard]"

What does fastapi[standard] include?

The [standard] extra installs everything you need for local development. This includes Pydantic for data validation, an HTTP server for running your API, and tools for auto-generating interactive documentation.

Make sure you include the quotes around "fastapi[standard]" to ensure it works in all terminal shells.

You'll see pip download and install several packages. This may take a minute.

  • Verify the installation succeeded by running:
python -c "import fastapi; print(fastapi.__version__)"

You should see a version number printed (such as 0.136.3 or similar). This confirms FastAPI is installed and importable.

Seeing a ModuleNotFoundError?

Make sure you see (venv) in your terminal prompt. If it's missing, your virtual environment isn't active. Go back and run the activation command for your OS.

If pip install itself failed, check your internet connection and try running the install command again.

Help me fix my FastAPI installation.

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

Create Your First Endpoints

Your development environment is set up and ready to go. Now it's time to write the actual API code that will handle requests from clients.

In this step, you'll create your first FastAPI application file, define a data model for books using Pydantic, and build two endpoints that let clients read from your collection.

In this step, get ready to:
  • Create main.py with the FastAPI app and a Pydantic data model.
  • Build GET endpoints to retrieve books from the collection.
  • Run the development server and test your endpoints in the interactive docs.
Create the app file and data model

Every FastAPI project starts with an app instance and at least one data model. The app instance is the core object that receives requests and routes them to your functions.

  • In VS Code's Explorer sidebar, right-click inside the bookshelf-api folder and select New File.
  • Name the file main.py and press Enter.
  • Add the following code to main.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

# Create the FastAPI app with metadata for the docs page
app = FastAPI(
    title="Bookshelf API",
    description="A REST API to manage your personal book collection",
    version="1.0.0",
)


# Define the shape of a Book using Pydantic
class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int | None = None
    rating: float | None = None


# In-memory storage (a dictionary acting as a simple database)
books_db: dict[int, dict] = {}
# Counter to assign unique IDs to new books
next_id: int = 1

What does this code do?

FastAPI() creates your application instance. The title, description, and version arguments populate the auto-generated documentation page.

The Book class inherits from Pydantic's BaseModel. This tells FastAPI exactly what shape a book should have. Fields with | None = None are optional.

books_db is a plain Python dictionary that stores books in memory. It works as a lightweight stand-in for a real database.

  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file exists by running this in your terminal:
ls

You should see main.py listed alongside your venv folder.

Don't see main.py?

Make sure you created the file inside the bookshelf-api folder, not inside venv. If you accidentally saved it in the wrong place, delete it and create it again in the correct location.

help me find where my main.py file ended up

Add the GET endpoints

Endpoints are the URLs that clients send requests to. A GET endpoint retrieves data without modifying anything on the server.

  • Add the following code to the bottom of main.py, below the next_id line:
@app.get("/books", response_model=list[Book])
def get_books():
    # Return all books as a list
    return list(books_db.values())


@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    # Check if the book exists, raise 404 if not
    if book_id not in books_db:
        raise HTTPException(
            status_code=404,
            detail=f"Book with id {book_id} not found",
        )
    return books_db[book_id]

What does this code do?

@app.get("/books") registers the function below it as the handler for GET requests to /books. The response_model parameter tells FastAPI what shape the response should have, which also powers the auto-docs.

The second endpoint uses a path parameter: {book_id} in the URL becomes the book_id: int argument in your function. FastAPI automatically validates that the value is an integer.

If a client requests a book that doesn't exist, HTTPException sends back a 404 status code with a helpful error message instead of crashing.

  • Save main.py.

You won't see a change yet. The next substep runs the server so you can test these endpoints live.

✔️ Awesome, I've got everything!

Great. Make sure you've saved main.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.py so far. Compare it against your file:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(
    title="Bookshelf API",
    description="A REST API to manage your personal book collection",
    version="1.0.0",
)


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


books_db: dict[int, dict] = {}
next_id: int = 1


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


@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(
            status_code=404,
            detail=f"Book with id {book_id} not found",
        )
    return books_db[book_id]
Run the development server

FastAPI includes a built-in development server that auto-reloads whenever you save changes to your code. It also generates interactive documentation you can use to test endpoints directly in your browser.

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

What does this command do?

fastapi dev starts the server with auto-reload enabled. Every time you save main.py, the server restarts automatically so your changes take effect immediately.

The server runs at http://127.0.0.1:8000 by default. It also generates an interactive documentation page at http://127.0.0.1:8000/docs.

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

You should see the Swagger UI page with your API title "Bookshelf API" at the top and two endpoints listed: GET /books and GET /books/{book_id}.

  • Click on the GET /books row to expand it.
  • Click Try it out, then click Execute.

You should see a response body of [] (an empty list). This makes sense because you haven't added any books yet.

  • Now expand GET /books/{book_id} and click Try it out.
  • Enter 1 in the book_id field, then click Execute.

You should see a 404 response with the message Book with id 1 not found. Your error handling is working correctly.

Server not starting?

Make sure your virtual environment is still activated. You should see (venv) at the beginning of your terminal prompt. If not, reactivate it by running:

source venv/bin/activate

Also double-check that you saved main.py before running the command. A syntax error in unsaved code won't cause issues, but a typo in the saved file will.

help me fix my FastAPI server startup error

Why is the book list empty?

Your API stores books in a Python dictionary that lives in memory. Every time the server restarts, the dictionary starts fresh and empty. This is expected for a learning project.

In a production API, you'd connect a database like PostgreSQL to persist data between restarts. For now, the in-memory approach keeps things simple so you can focus on REST API patterns.

Your API can read from the book collection. Next up, you'll add the ability to create, update, and delete books to complete the full CRUD lifecycle.

Add Create, Update, and Delete

You can read books from your API. Now it is time to complete the full CRUD lifecycle by adding the ability to create, update, and delete books.

Each operation maps to a specific HTTP method: POST for creating, PUT for updating, and DELETE for removing. These are the building blocks of every REST API you will ever work with.

In this step, get ready to:
  • Create a separate Pydantic model for incoming book data.
  • Add POST, PUT, and DELETE endpoints to your API.
  • Test the full create-read-update-delete cycle in Swagger UI.
Create the BookCreate model and POST endpoint

When a client sends data to create a new book, they should not be responsible for providing the id. Your server assigns that automatically. A separate model for incoming data keeps this boundary clean.

  • In main.py, find the line that says class Book(BaseModel):.
  • Add the following BookCreate class directly above it:
# Model for incoming book data (no id field)
class BookCreate(BaseModel):
    title: str
    author: str
    year: int | None = None
    rating: float | None = None

Why a separate model for creation?

The Book model includes an id field because that is what the API returns. But when creating a book, the client does not know the ID yet. BookCreate excludes id so FastAPI rejects any request that tries to set one.

This pattern of separating input models from output models is standard practice in production APIs.

  • You will not see a change yet. The next chunk uses this model in the POST endpoint.
  • Now scroll to the bottom of main.py (after the get_book function) and add the POST endpoint by copying this code:
@app.post("/books", response_model=Book, status_code=201)
def create_book(book: BookCreate):
    global next_id
    # Build a new book dict with an auto-assigned id
    new_book = {"id": next_id, **book.model_dump()}
    books_db[next_id] = new_book
    next_id += 1
    return new_book

What does this code do?

The @app.post decorator registers a POST endpoint at /books. The status_code=201 tells clients that a resource was successfully created.

The book: BookCreate parameter tells FastAPI to expect a JSON body matching the BookCreate schema. The model_dump() method converts the Pydantic model into a plain dictionary so you can store it.

  • Save main.py.
  • Refresh the Swagger UI at http://127.0.0.1:8000/docs. You should see a new POST /books endpoint in the list.

Don't see the new POST endpoint?

Make sure you saved main.py. The dev server auto-reloads when it detects a file change. Check your terminal for any error messages. A common mistake is placing BookCreate below Book but referencing it in create_book before it is defined.

help me debug why my POST endpoint isn't showing up in the Swagger docs

Add the PUT and DELETE endpoints

With creation handled, you need two more operations: updating an existing book and removing one entirely.

  • In main.py, add the PUT endpoint below the create_book function:
@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, book: BookCreate):
    # Return 404 if the book doesn't exist
    if book_id not in books_db:
        raise HTTPException(
            status_code=404,
            detail=f"Book with id {book_id} not found",
        )
    # Replace the stored book data with the new values
    updated_book = {"id": book_id, **book.model_dump()}
    books_db[book_id] = updated_book
    return updated_book

What does this code do?

The PUT endpoint takes both a book_id path parameter and a BookCreate request body. It checks if the book exists first. If it does not, it raises a 404 error with a descriptive message.

If the book exists, it replaces the stored data entirely with the new values while preserving the original id.

  • Save main.py. Refresh the Swagger UI and confirm you see a PUT /books/{book_id} endpoint.
  • Now add the DELETE endpoint below update_book:
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    # Return 404 if the book doesn't exist
    if book_id not in books_db:
        raise HTTPException(
            status_code=404,
            detail=f"Book with id {book_id} not found",
        )
    # Remove the book from storage
    del books_db[book_id]

Why status code 204?

Status code 204 means "No Content". It tells the client that the deletion succeeded but there is nothing to send back in the response body. This is the standard HTTP response for successful deletions.

  • Save main.py. Refresh the Swagger UI. You should now see all five endpoints listed: two GET, one POST, one PUT, and one DELETE.

Seeing an error in your terminal?

Check for indentation issues or missing colons at the end of def lines. Also verify that each function name is unique. If your terminal shows a traceback, read the last line for the specific error.

help me fix the error in my FastAPI endpoints

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that all five endpoints appear in the Swagger UI.

ⓧ I'd like to double check the full code

Here is the complete main.py at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI(
    title="Bookshelf API",
    description="A REST API to manage your personal book collection",
    version="1.0.0",
)


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


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


books_db: dict[int, dict] = {}
next_id: int = 1


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


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


@app.post("/books", response_model=Book, status_code=201)
def create_book(book: BookCreate):
    global next_id
    new_book = {"id": next_id, **book.model_dump()}
    books_db[next_id] = new_book
    next_id += 1
    return new_book


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


@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(
            status_code=404,
            detail=f"Book with id {book_id} not found",
        )
    del books_db[book_id]
Test the full CRUD cycle in Swagger UI

Time to prove that all four operations work together. You will create a book, read it back, update it, then delete it.

  • In the Swagger UI at http://127.0.0.1:8000/docs, expand the POST /books section.
  • Click Try it out.
  • Replace the example JSON in the request body with this:
{
  "title": "The Hobbit",
  "author": "J.R.R. Tolkien",
  "year": 1937,
  "rating": 4.8
}
  • Click Execute. You should see a 201 response with your book data, including an auto-assigned id of 1.
  • Expand GET /books/{book_id}, click Try it out, enter 1 for the book_id, and click Execute. You should see your book returned.
  • Expand PUT /books/{book_id}, click Try it out, enter 1 for the book_id, and change the rating to 5.0 in the body.
  • Click Execute. The response should show the updated rating.
  • Expand DELETE /books/{book_id}, click Try it out, enter 1, and click Execute. You should see a 204 response with no body.
  • Go back to GET /books and execute it. The response should be an empty list, confirming the book was deleted.

What should I see?

After the full cycle, your GET /books response returns []. This confirms that every CRUD operation worked correctly. The book was created, read, updated, and then removed from memory.

Your API now supports the complete CRUD lifecycle. Next up, you will explore the interactive documentation in more depth and test error handling.

Test Your API with Interactive Docs

You've built a complete set of CRUD endpoints. Now it's time to run them through a full lifecycle test to prove everything works together.

FastAPI generates interactive documentation that doubles as a testing playground. You can send real HTTP requests directly from your browser without installing extra tools like Postman or curl.

In this step, get ready to:
  • Create several books through the interactive docs to populate your collection.
  • Test error handling by requesting a book that does not exist.
  • Explore the alternative ReDoc documentation view.
Create books using the interactive docs

The Swagger UI at /docs lists every endpoint your API exposes. Each endpoint has a Try it out button that lets you fill in parameters and send a live request.

  • Return to your browser tab with the Swagger UI at http://127.0.0.1:8000/docs.
  • Scroll down to the POST /books endpoint and click it to expand the details.
  • Click the Try it out button on the right side of the endpoint panel.
  • In the request body editor, replace the placeholder JSON with your first book:
{
  "title": "The Hobbit",
  "author": "J.R.R. Tolkien",
  "year": 1937,
  "rating": 4.8
}
  • Click the Execute button to send the request.

You should see a response with status code 201 and the book data returned with an id field of 1 added automatically.

  • Repeat this process to add 2-3 more books with different authors, years, and ratings. Here are some suggestions:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "year": 1965,
  "rating": 4.6
}
{
  "title": "1984",
  "author": "George Orwell",
  "year": 1949,
  "rating": 4.7
}
  • After adding your books, expand the GET /books endpoint.
  • Click Try it out, then click Execute.

You should see a JSON array containing all the books you just created, each with its own unique id.

What is Swagger UI doing behind the scenes?

Every time you click Execute, Swagger UI sends a real HTTP request to your running server. The response you see is identical to what any client (a mobile app, a frontend, or another service) would receive.

FastAPI generates this documentation automatically from your route decorators and Pydantic models. You never have to write or maintain docs manually.

Not seeing your books in GET /books?

Make sure your fastapi dev server is still running in the terminal. If you restarted it, the in-memory storage resets and all books are cleared.

Also check that you clicked Execute after clicking Try it out. Both clicks are required.

Why is my GET /books response an empty list even though I created books?

Test error handling with a non-existent book ID

A well-designed API returns clear error messages when something goes wrong. Your GET /books/{book_id} endpoint raises a 404 error for IDs that do not exist. Let's verify that.

  • Expand the GET /books/{book_id} endpoint in the Swagger UI.
  • Click Try it out.
  • Enter 999 in the book_id field.
  • Click Execute.

You should see a response with status code 404 and a JSON body like this:

{
  "detail": "Book with id 999 not found"
}

Why does proper error handling matter?

When a client asks for a resource that does not exist, returning a clear 404 response with a descriptive message helps developers debug quickly. Without it, they would get a generic 500 server error or an empty response with no explanation.

The HTTPException you raised in your code maps directly to this structured error response. FastAPI handles the formatting for you.

Explore the ReDoc documentation view

FastAPI provides a second documentation interface called ReDoc. It presents the same API information in a clean, readable layout that works well for sharing with others.

  • Open a new browser tab and navigate to http://127.0.0.1:8000/redoc.
  • Browse the documentation page. Notice how it displays your API title, description, and version from the FastAPI() constructor.
  • Expand any endpoint to see the request and response schemas generated from your Pydantic models.

You should see all five of your endpoints organized by HTTP method, with the BookCreate and Book schemas listed in the sidebar.

Swagger UI vs ReDoc

Swagger UI (at /docs) is interactive. You can test endpoints directly. ReDoc (at /redoc) is read-only but has a cleaner layout for documentation consumers.

Both are generated automatically from the same source: your route decorators and Pydantic models. You get two professional documentation views without writing a single line of documentation code.

  • Take a screenshot of either the Swagger UI page or the ReDoc page. This is a portfolio-ready artifact that shows your API design skills.

ReDoc page not loading?

Double check the URL is exactly http://127.0.0.1:8000/redoc with no trailing slash or typo. Make sure your fastapi dev server is still running.

Why can't I load the ReDoc documentation page?

Secret mission

Add Filtering and Search

Your API returns every book in the collection right now. Real APIs let users narrow results with query parameters. Can you add filtering by author, minimum rating, and a search feature to your GET /books 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 project files for future reference, pause the running server, or delete everything entirely.

Resources you used:

  • The bookshelf-api project folder on your Desktop (contains your code and virtual environment).
  • The FastAPI dev server running at http://127.0.0.1:8000.
  • The FastAPI package installed inside your virtual environment.

Keep everything running

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

  • Your FastAPI dev server stays running at http://127.0.0.1:8000.
  • Your project files remain in the bookshelf-api folder on your Desktop.
  • The virtual environment and installed packages stay intact.

Pause - I'll come back to this later

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

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

When you're ready to resume, navigate back to the bookshelf-api folder, reactivate the virtual environment, and start the server again.

Delete - I don't want to use this again

Remove all project resources 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 server.
  • Deactivate the virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:

macOS

cd ~/Desktop
rm -rf bookshelf-api

Windows

cd ~\Desktop
rmdir /s /q bookshelf-api

This removes your main.py file, the venv virtual environment, and the FastAPI package that was installed inside it.

Nice Work!

Nice Work!

You just built a real REST API from scratch using FastAPI. Your bookshelf API handles the full lifecycle of creating, reading, updating, and deleting data through clean endpoints.

You've learned how to:

  • Built 5 REST endpoints covering the full CRUD lifecycle with proper HTTP status codes (201 Created, 204 No Content, 404 Not Found).
  • Used Pydantic models for automatic request data validation and error handling with meaningful error messages.
  • Got auto-generated interactive API documentation (Swagger UI) that lets anyone test your endpoints directly in the browser.
  • Secret Mission: Added filtering and search with query parameters so users can find books by author, minimum rating, or keyword.

This is exactly the kind of project that shows employers you understand modern Python API development. The auto-generated docs at /docs make it easy to demo in interviews or link from your portfolio.

Ready to quiz yourself?