Build a Bookshelf REST API with FastAPI

Build a CRUD REST API in Python using FastAPI and Pydantic models.

Introduction

30 Second Summary

Every time you open an app on your phone, it talks to a server somewhere on the internet to fetch or update data. That conversation follows a pattern called REST, and it powers nearly every modern application you use.

In this project, you will build your own REST API server using Python and FastAPI. Your API will manage a bookshelf of books, letting you add, view, update, and delete entries through HTTP requests.

What You'll Build

A Bookshelf API that receives HTTP requests and responds with JSON data, with automatic data validation and interactive documentation you can test in your browser.

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

  • A running REST API server on your computer that responds to HTTP requests with JSON data.
  • Five working CRUD endpoints covering all major HTTP methods (GET, POST, PUT, DELETE) for managing books.
  • Hands-on experience testing your API using FastAPI's built-in interactive documentation at /docs.
  • Secret Mission: Add a search endpoint that filters books by author or title.

Are there any prerequisites?

No prior experience with APIs or web development is needed. This guide walks you through every step, including installing all required tools. Basic familiarity with Python syntax (variables, functions, lists) is helpful but not required.

Set Up Your Project

Before you can build your API, you need a workspace. Every Python project benefits from its own isolated environment where you can install packages without affecting anything else on your computer.

In this step, you will create a project folder, set up a Python virtual environment, and install FastAPI. By the end, you will have everything ready to start writing your first endpoint.

In this step, get ready to:
  • Create a project folder and virtual environment.
  • Install FastAPI.
  • Create your project file.
Create a project folder and virtual environment
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open a terminal inside VS Code by clicking Terminal in the top menu bar, then New Terminal.

Keyboard shortcut for the terminal

You can also press Ctrl+` (backtick key, above Tab) on both macOS and Windows to toggle the terminal panel.

  • 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 the folder was created by checking your terminal prompt now shows bookshelf-api as the current directory.

A virtual environment is an isolated space where Python packages get installed without affecting other projects on your computer. You will create one now and activate it.

  • Create and activate a virtual environment by running these commands:

macOS

python3 -m venv env
source env/bin/activate

Windows

python -m venv env
.\env\Scripts\activate

What do these commands do?

python3 -m venv env creates a new virtual environment in a folder called env inside your project. This folder holds its own copy of Python and any packages you install.

The second command activates the environment so that any pip install commands you run will go into this isolated space instead of your system-wide Python.

  • Check your terminal prompt. You should see (env) at the beginning of the line. This confirms the virtual environment is active.

Don't see (env) in your prompt?

  • On Windows, if you see an error about running scripts being disabled, run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser first, then try the activate command again.
  • Make sure you are inside the bookshelf-api folder. Run pwd (macOS) or cd (Windows) to check your current location.
  • Still stuck? Help me activate my Python virtual environment
Install FastAPI

FastAPI is the Python framework you will use to build your API. Installing it with the standard extras gives you everything you need, including the development server.

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

What does this command do?

pip install "fastapi[standard]" installs FastAPI along with the standard set of extras you need for development. This includes Uvicorn (the server that runs your API) and other utilities.

The [standard] part tells pip to include the recommended extra packages. Without it, you would need to install them separately.

  • Verify the installation succeeded by running:
pip show fastapi
  • You should see output showing the package name, version (such as 0.136.3 or similar), and other details. This confirms FastAPI is installed in your virtual environment.

Seeing "No module named pip" or install errors?

  • Make sure (env) is showing in your terminal prompt. If it is not, rerun the activate command from the previous substep.
  • If you see a permission error, do not use sudo. Instead, make sure you activated the virtual environment first.
  • Still stuck? Help me install FastAPI in my virtual environment
Create your project file

All of your API code will live in a single file called main.py. FastAPI automatically detects this filename when you start the development server.

  • Create the file by running this command in your terminal:

macOS

touch main.py

Windows

New-Item main.py
  • Confirm the file exists by running:
ls
  • You should see main.py and the env folder listed in the output.
  • Open the bookshelf-api folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the bookshelf-api folder, and click Open.
  • In the Explorer sidebar on the left, you should see main.py listed. Click it to open the file in the editor.

Don't see main.py in the Explorer?

  • Make sure you opened the bookshelf-api folder itself, not your Desktop folder. The Explorer title bar should say BOOKSHELF-API.
  • If the terminal reopened in a different location, re-activate your virtual environment using the activate command from earlier.
  • Need a hand? Help me find my main.py file in VS Code

Your workspace is ready. You have a dedicated project folder, an active virtual environment with FastAPI installed, and an empty main.py waiting for your first lines of code. Next up, you will write your first API endpoint and see it respond in the browser.

Create Your First API Endpoint

In the previous step, you installed FastAPI and created an empty main.py file. Now it is time to make that file do something.

Every REST API needs at least one endpoint. An endpoint is a URL that your server listens on and responds to when a request arrives. In this step, you will create your first endpoint, define the data shape for books, and explore the interactive documentation that FastAPI generates for you automatically.

In this step, get ready to:
  • Create a FastAPI app with a root endpoint that returns a welcome message.
  • Define data models for books using Pydantic.
  • Explore the auto-generated interactive API documentation.
Set up the FastAPI app and create a root endpoint

An endpoint connects a URL path to a Python function. When someone visits that path, FastAPI runs the function and sends its return value back as JSON.

  • Add the following code to your empty main.py file:
# Import FastAPI framework and HTTP exception handler
from fastapi import FastAPI, HTTPException

# Import Pydantic for data validation
from pydantic import BaseModel


# Create the FastAPI 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?

  • from fastapi import FastAPI, HTTPException imports the main framework class and an error helper you will use later.
  • from pydantic import BaseModel imports the base class for data validation models (you will use this in the next substep).
  • app = FastAPI() creates a new application instance that handles all incoming requests.
  • @app.get("/") is a decorator that tells FastAPI to call the function below whenever a GET request hits the / path.
  • The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
  • Save main.py.
  • Start your development server by running this command in the terminal:
fastapi dev

What happens when you run this?

The fastapi dev command starts a local development server using Uvicorn. It automatically finds your main.py file and the app object inside it.

The server runs at http://127.0.0.1:8000 and auto-reloads whenever you save changes to your code. The terminal will stay occupied while the server is running. Press Ctrl+C to stop it when needed.

  • Wait until you see a line containing Uvicorn running on http://127.0.0.1:8000 in the terminal output.
  • Open your browser and visit http://127.0.0.1:8000.

You should see a JSON response displayed in your browser: {"message":"Welcome to the Bookshelf API"}.

Not seeing the JSON response?

  • Make sure you saved main.py before running fastapi dev.
  • Check the terminal for error messages. A syntax error in your code will prevent the server from starting.
  • Confirm the URL in your browser is exactly http://127.0.0.1:8000 (not https, and port 8000).

My FastAPI server won't start or I'm seeing an error in the terminal.

Define the Book data model with Pydantic

Your API needs to know what a "book" looks like. Pydantic models let you define the exact fields and types that your API accepts. FastAPI uses these models to automatically validate incoming data and reject anything that does not match.

  • In main.py, find the line app = FastAPI() and add the following code below it (above the @app.get("/") line):
# Define the shape of a book (full model with ID)
class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    read: bool = False


# Define the shape of incoming book data (no ID needed)
class BookCreate(BaseModel):
    title: str
    author: str
    year: int
    read: bool = False

What do these models do?

Book is the full model that includes an id field. This is what your API returns when someone asks for a book.

BookCreate is the model for incoming data when someone adds a new book. It does not have an id field because the server generates IDs automatically. The read field defaults to False if the user does not provide it.

Now you need some sample data so the API has books to work with right away.

  • Below the BookCreate class, add this list of sample books (still above the @app.get("/") line):
# Pre-load two sample books into memory
books = [
    Book(id=1, title="To Kill a Mockingbird", author="Harper Lee", year=1960, read=True),
    Book(id=2, title="1984", author="George Orwell", year=1949, read=False),
]

Why use an in-memory list?

Storing books in a Python list keeps the project focused on learning REST API concepts. Every time you restart the server, the list resets to these two sample books. In a production app, you would use a database instead.

  • Save main.py.
  • Check your terminal. The server should auto-reload without errors. You should see a new log line indicating the reload completed.

Seeing an error after saving?

  • Make sure the Book and BookCreate classes are placed after app = FastAPI() and before @app.get("/").
  • Check that the indentation inside each class uses 4 spaces (not tabs).
  • Verify that the books list is also placed before the @app.get("/") decorator.

My server shows an error after I added the Book models.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the complete main.py file as it should look right now:

from fastapi import FastAPI, HTTPException

from pydantic import BaseModel


app = FastAPI()


class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    read: bool = False


class BookCreate(BaseModel):
    title: str
    author: str
    year: int
    read: bool = False


books = [
    Book(id=1, title="To Kill a Mockingbird", author="Harper Lee", year=1960, read=True),
    Book(id=2, title="1984", author="George Orwell", year=1949, read=False),
]


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

FastAPI automatically generates interactive documentation for every endpoint you create. This documentation lets you test your API directly in the browser without needing any extra tools.

  • Open a new browser tab and visit http://127.0.0.1:8000/docs.

You should see the Swagger UI page listing your GET / endpoint.

  • Click the GET / row to expand it.
  • Click Try it out in the top right corner of the expanded section.
  • Click Execute.

You should see a response body showing {"message": "Welcome to the Bookshelf API"} with a status code of 200.

Why is this documentation useful?

The /docs page is a live testing tool. As you add more endpoints in the next steps, they will appear here automatically. You can send real requests and see real responses without writing any client code or using a separate tool like Postman.

Not seeing the /docs page?

  • Make sure the server is still running in your terminal. If you accidentally pressed Ctrl+C, run fastapi dev again.
  • Confirm the URL is http://127.0.0.1:8000/docs (with /docs at the end).

The /docs page is not loading or shows an error.

Your API server is running and responding to requests. You have a data model that defines what a book looks like, sample data to work with, and interactive documentation to test everything. Next up, you will build the endpoints that let you read books from the shelf.

Build the Read Endpoints

Your API is live and returning a welcome message. Now it is time to make it actually useful by letting clients retrieve book data.

In REST APIs, reading data is handled by GET requests. You will build two GET endpoints: one that returns the full list of books, and one that returns a single book by its ID.

In this step, get ready to:
  • Create a GET endpoint that returns all books in your collection.
  • Create a GET endpoint that returns a single book by its ID.
Create the list all books endpoint

The most common thing a client needs to do is fetch all available data. A GET endpoint at /books will return the entire books list as JSON.

  • In main.py, add this code below your read_root function:
@app.get("/books")
def list_books():
    return books

What does this code do?

  • @app.get("/books") registers this function to handle GET requests sent to the /books URL path.
  • The function simply returns the books list. FastAPI automatically converts it to JSON for the response.
  • Save main.py.
  • Open http://127.0.0.1:8000/books in your browser. You should see a JSON array containing your two sample books (To Kill a Mockingbird and 1984).

Don't see the books list?

  • Make sure you saved main.py. The dev server auto-reloads on save, but unsaved changes won't take effect.
  • Check that your new function is placed after the closing line of read_root, not inside it. Python uses indentation to determine scope.
  • Confirm the URL is exactly /books (not /book or /Books).

Help me debug why my /books endpoint isn't returning data.

Create the get one book endpoint

Clients often need to fetch a single specific item rather than the entire collection. To do this, you use a path parameter. This is a variable part of the URL that identifies which resource the client wants.

  • Add this code below your list_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
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

  • {book_id} in the URL path tells FastAPI that this part of the URL is a variable. Whatever value the client puts there gets passed into the function as the book_id parameter.
  • The book_id: int type hint makes FastAPI automatically validate that the value is an integer. If someone passes a string like "abc", FastAPI returns an error without your code needing to handle it.
  • The for loop searches the books list for a matching ID. If found, it returns that book immediately.
  • If no book matches, raise HTTPException(status_code=404) sends back a 404 Not Found response. The HTTPException class is already imported at the top of your file from the previous step.

What is a 404 status code?

HTTP status codes are three-digit numbers that tell the client what happened. 200 means success, 201 means something was created, and 404 means the requested resource does not exist.

By raising a 404 when a book ID does not match, you are telling the client "I understood your request, but there is no book with that ID."

  • Save main.py.
  • Switch back to the /docs page in your browser at http://127.0.0.1:8000/docs.
  • Click the GET /books/{book_id} endpoint, then click Try it out.
  • Enter 1 for the book_id and click Execute. You should see the details for "To Kill a Mockingbird" in the response.
  • Now enter 99 for the book_id and click Execute again. You should see a 404 response with the detail "Book not found".

Seeing a validation error instead of a 404?

  • If you see a 422 "Unprocessable Entity" error, make sure you entered a number (like 99) and not text. FastAPI validates that book_id must be an integer.
  • If the endpoint does not appear in /docs at all, check that you saved main.py and that the dev server auto-reloaded (look for a reload message in your terminal).

Help me debug my get one book endpoint.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and both endpoints are working in your browser.

ⓧ I'd like to double check the full code

Here is the full main.py as it should look at the end of this step:

from fastapi import FastAPI, HTTPException

from pydantic import BaseModel


app = FastAPI()


class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    read: bool = False


class BookCreate(BaseModel):
    title: str
    author: str
    year: int
    read: bool = False


books = [
    Book(id=1, title="To Kill a Mockingbird", author="Harper Lee", year=1960, read=True),
    Book(id=2, title="1984", author="George Orwell", year=1949, read=False),
]


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


@app.get("/books")
def list_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")

Your API can now serve data to any client that asks for it. Next up, you will build the write endpoints that let clients add, update, and delete books from the shelf.

Build the Write Endpoints

Your read endpoints are working. Visitors to your API can browse the full book list and look up any single book by its ID.

Reading is only half the story. In this step, you'll add the ability to create, update, and delete books, completing the full set of CRUD operations.

In this step, get ready to:
  • Create a POST endpoint to add new books to your shelf.
  • Build a PUT endpoint to update existing book details.
  • Add a DELETE endpoint to remove books from the list.
Add the POST endpoint to create books

When someone wants to add a new book to the shelf, they send a POST request with the book's details. Your endpoint needs to accept that data, assign a unique ID, and store the book.

  • In main.py, add the create-book endpoint below the get_book function by pasting this code:
@app.post("/books", status_code=201)
def create_book(book_data: BookCreate):
    # Generate a unique ID based on the highest existing ID
    new_id = max((book.id for book in books), default=0) + 1
    # Create a full Book instance from the incoming data
    new_book = Book(id=new_id, **book_data.model_dump())
    books.append(new_book)
    return new_book

What does this code do?

  • @app.post("/books", status_code=201) tells FastAPI this endpoint handles POST requests and returns a 201 Created status code on success.
  • book_data: BookCreate means FastAPI will automatically validate incoming JSON against your BookCreate model. If any field is missing or the wrong type, the request is rejected before your function even runs.
  • max((book.id for book in books), default=0) + 1 finds the highest existing ID and adds 1 to guarantee a unique new ID.
  • book_data.model_dump() converts the Pydantic model into a dictionary so you can spread it into the Book constructor with **.
  • Save main.py.
  • Switch back to the /docs page in your browser. You should see a new green POST /books endpoint in the list.
  • Click the POST /books endpoint, then click Try it out.
  • Replace the example JSON in the request body with this:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "year": 1925,
  "read": true
}
  • Click Execute. You should see a 201 response with your new book, including an auto-generated id of 3.

Don't see the POST endpoint in /docs?

  • Make sure you saved main.py. The dev server auto-reloads on save, but unsaved changes won't appear.
  • Check that the new code is below the get_book function, not inside it. The @app.post decorator should be at the same indentation level as @app.get.
  • If the terminal shows a syntax error, check for missing colons or mismatched parentheses.

Help me debug why my POST endpoint isn't appearing in FastAPI docs.

Add the PUT endpoint to update books

Users need a way to correct a book's details after adding it. The PUT method replaces an existing resource with new data.

  • Add the update endpoint below the create_book function by pasting this code:
@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 the old book with updated data
            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 you both the index position and the book object as you loop, so you know exactly where to replace.
  • Book(id=book_id, **book_data.model_dump()) creates a fresh Book that keeps the original ID but takes all other fields from the incoming data.
  • If no book matches the ID, the function raises a 404 error. This prevents someone from accidentally "updating" a book that doesn't exist.
  • Save main.py.
  • In the /docs page, you should now see a blue PUT /books/{book_id} endpoint.
  • Click the PUT /books/{book_id} endpoint, then click Try it out.
  • Enter 1 in the book_id field.
  • Replace the request body with this:
{
  "title": "To Kill a Mockingbird",
  "author": "Harper Lee",
  "year": 1960,
  "read": true
}
  • Click Execute. You should see a 200 response with the updated book data.

Getting a 422 Validation Error?

  • Make sure the book_id field contains a number, not text. FastAPI expects an integer because the function parameter is typed as int.
  • Check that your JSON body includes all required fields (title, author, year). Missing fields trigger a validation error.

Help me fix this 422 error from my PUT endpoint.

Add the DELETE endpoint to remove books

The final piece of CRUD is deletion. A DELETE endpoint removes a resource and returns no content, signalling that the operation succeeded but there is nothing to send back.

  • Add the delete endpoint below the update_book function by pasting this code:
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    # Find the book by ID and remove it
    for index, book in enumerate(books):
        if book.id == book_id:
            books.pop(index)
            return
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

  • status_code=204 means "No Content." The server confirms the deletion succeeded but sends back an empty response body.
  • books.pop(index) removes the book from the list at that position.
  • The bare return (with no value) matches the 204 status. FastAPI knows not to include a response body.

Notice the same 404 pattern you used in the GET and PUT endpoints. Consistent error handling makes your API predictable for anyone using it.

  • Save main.py.
  • In the /docs page, you should now see a red DELETE /books/{book_id} endpoint.
  • Click the DELETE /books/{book_id} endpoint, then click Try it out.
  • Enter 1 in the book_id field.
  • Click Execute. You should see a 204 response with no body.
  • Now test the GET /books endpoint. The book with ID 1 should be gone from the list.

Book still appearing after DELETE?

  • Make sure you saved main.py after adding the DELETE endpoint.
  • Note that when the dev server reloads, it resets the in-memory list to the original two books. If you saw the server reload in the terminal, the deletion was undone. Delete the book again after the reload.
  • Check that you entered the correct book_id number. The sample books have IDs 1 and 2.

Why is my deleted book still showing up in the list?

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and that all five endpoints appear in your /docs page.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException

from pydantic import BaseModel


app = FastAPI()


class Book(BaseModel):
    id: int
    title: str
    author: str
    year: int
    read: bool = False


class BookCreate(BaseModel):
    title: str
    author: str
    year: int
    read: bool = False


books = [
    Book(id=1, title="To Kill a Mockingbird", author="Harper Lee", year=1960, read=True),
    Book(id=2, title="1984", author="George Orwell", year=1949, read=False),
]


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


@app.get("/books")
def list_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_data: BookCreate):
    new_id = max((book.id for book in books), default=0) + 1
    new_book = Book(id=new_id, **book_data.model_dump())
    books.append(new_book)
    return new_book


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


@app.delete("/books/{book_id}", status_code=204)
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")

Secret mission

Add a Search Endpoint

Your API handles individual books and the full list. But what if someone wants to find all books by a specific author, or search by title? In this mission, you will add a search endpoint that accepts query parameters and filters your bookshelf.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

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

Keep everything running

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

  • Your FastAPI dev server continues running at http://127.0.0.1:8000.
  • Your virtual environment stays activated.
  • You can keep testing endpoints in the /docs page.

Pause - I'll come back to this later

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

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

When you come back, open your project folder in VS Code, activate the virtual environment again, and run fastapi dev to resume.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes everything you built in this project.

  • In the terminal where your server is running, press Ctrl+C to stop the FastAPI dev server.
  • Deactivate your 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 project folder, the virtual environment, and all installed packages inside it.

Nice Work!

Nice Work!

You just built a fully functional REST API server from scratch using Python and FastAPI. Your Bookshelf API can create, read, update, and delete books, just like the backends that power real-world applications.

You've learned how to:

  • Built a complete REST API server with five CRUD endpoints covering GET, POST, PUT, and DELETE requests.
  • Used Pydantic models to automatically validate incoming request data and return proper HTTP status codes (200, 201, 204, 404).
  • Explored and tested every endpoint using FastAPI's built-in interactive documentation (Swagger UI) at /docs.
  • Secret Mission: Added a search endpoint with query parameter filtering for case-insensitive partial matching.

Ready to quiz yourself?