Build a Bookstore REST API with FastAPI

Build a CRUD REST API for a bookstore using FastAPI and Pydantic.

Introduction

30 Second Summary

Every time you use a website or app to browse products, place an order, or check your account, you're talking to an API behind the scenes. APIs are the invisible bridges that let different software systems share data with each other.

In this project, you will build a REST API from scratch using Python and FastAPI. Your API will manage a bookstore's inventory, letting you add, browse, update, and remove books through HTTP requests.

What You'll Build

A fully functional Bookstore REST API with five endpoints covering all CRUD operations, tested through an interactive browser-based documentation page that FastAPI generates automatically.

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

  • A Python REST API server running locally with FastAPI.
  • Pydantic data models that define and validate the shape of your book data.
  • Five API endpoints covering Create, Read, Update, and Delete operations with proper HTTP status codes.
  • Auto-generated interactive documentation (Swagger UI) for testing every endpoint in your browser.
  • Secret Mission: Add search and filtering capabilities so users can find books by author name or within a price range.

Are there any prerequisites?

You should be comfortable with basic Python scripting. If you know how to use variables, functions, lists, and dictionaries, you have everything you need to get started.

Before We Start

Before We Start...

Set Up Your Project

Every API needs a clean workspace and the right tools installed before you start writing code.

In this step, you'll create a dedicated project folder, set up an isolated Python environment, and install FastAPI with everything it needs to run a local development server.

In this step, get ready to:
  • Create a project folder and a Python virtual environment.
  • Install FastAPI with all standard dependencies.
  • Verify the FastAPI CLI is working.
Create a project folder and virtual environment

A virtual environment keeps your project's packages separate from the rest of your system. This prevents version conflicts between different Python projects.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar, then type Terminal (macOS) or PowerShell (Windows) and press Enter.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.10 or higher

Your Python version is compatible. Move on to the next instruction below.

ⓧ I see a version lower than 3.10

FastAPI requires Python 3.10 or higher. You need to install an updated version.

  • Visit the official Python downloads page at python.org/downloads.
  • Download and install Python 3.10 or higher for your operating system.
  • On macOS, the system Python may still point to an older version after installing. Use python3.11 or python3.12 (matching the version you installed) instead of python3 for the rest of this project.
  • After installing, close and reopen your terminal, then run the version check again to confirm.

ⓧ Command not found

Python is not installed on your system. You need to install it before continuing.

  • Visit the official Python downloads page at python.org/downloads.
  • Download and install Python 3.10 or higher for your operating system.
  • After installing, close and reopen your terminal, then run python3 --version again to confirm.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir bookstore-api
cd bookstore-api
  • Create a Python virtual environment by running this command:
python3 -m venv venv

What does this command do?

python3 -m venv venv tells Python to create a virtual environment in a folder called venv. This isolated space holds all the packages you install for this project without affecting your system Python.

  • Activate the virtual environment by running this command:

macOS

source venv/bin/activate

Windows

.\venv\Scripts\activate

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

Don't see (venv) in your prompt?

  • Make sure you're inside the bookstore-api folder. Run ls and confirm you see a venv folder listed.
  • On Windows, if you see a "running scripts is disabled" error, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and try the activate command again.

Help me activate my Python virtual environment.

Install FastAPI with all standard dependencies

FastAPI ships with a set of standard extras that include everything you need for development. This includes the Uvicorn web server and the FastAPI CLI.

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

What does this install?

The [standard] extra installs FastAPI along with Uvicorn (the development server) and the FastAPI CLI. These tools let you start your API server with a single command.

You should see pip downloading and installing several packages. The process may take a minute.

Seeing installation errors?

  • Make sure (venv) appears in your terminal prompt. If it doesn't, you're installing globally instead of into your virtual environment.
  • If you see a "Python version" error, your Python is older than 3.10. Go back to the version check in the previous substep.

Help me fix my FastAPI installation error.

Verify the installation
  • Confirm FastAPI installed correctly by checking the CLI version:
fastapi --version

You should see output showing the FastAPI CLI version number. This confirms that both FastAPI and its CLI tool are installed and ready to use.

Command not found?

  • Make sure your virtual environment is still active. Look for (venv) in your terminal prompt.
  • Try deactivating and reactivating your virtual environment, then run the command again.

Help me get the fastapi command working.

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

Create Your First API Endpoint

Your development environment is set up and FastAPI is installed. Now it's time to write code that responds to HTTP requests.

Every web application needs at least one endpoint for clients to talk to. You'll create a simple endpoint, start a local server, and see your API respond in real time.

In this step, get ready to:
  • Create a main.py file with a basic FastAPI application.
  • Run the development server and see your API respond in the browser.
  • Explore the auto-generated interactive Swagger UI documentation.
Create your first FastAPI app

A REST API endpoint is a URL that your server listens on. When a client visits that URL, the server runs a function and sends back a response.

  • In VS Code, create a new file called main.py in your project folder by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save the empty file immediately as main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Copy the following code into main.py:
# Import the FastAPI framework
from fastapi import FastAPI

# Create your application instance
app = FastAPI()


# Define a GET endpoint at the root URL "/"
@app.get("/")
def root():
    # Return a JSON response with a welcome message
    return {"message": "Welcome to the Bookstore API!"}

What does this code do?

  • from fastapi import FastAPI imports the framework so you can use it.
  • app = FastAPI() creates the application instance that will handle all incoming requests.
  • @app.get("/") is a decorator that tells FastAPI to run the function below whenever someone visits the root URL with a GET request.
  • The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Seeing red squiggles under the import?

Make sure your virtual environment is selected as the Python interpreter in VS Code. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the interpreter from your venv folder.

help me fix red squiggles on my FastAPI import in VS Code

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the complete main.py file at this point:

from fastapi import FastAPI

app = FastAPI()


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

FastAPI includes a built-in development server that auto-reloads whenever you save changes to your code. This means you can edit and test without restarting anything.

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

You should see output ending with a line that says Uvicorn running on http://127.0.0.1:8000.

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

You should see a JSON response: {"message":"Welcome to the Bookstore API!"}.

What is 127.0.0.1:8000?

This is your computer's localhost address. The 127.0.0.1 part means "this machine only" and 8000 is the port number. Your API is only accessible from your computer, not from the internet.

Server not starting?

  • Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
  • Check that you saved main.py in the same folder where your terminal is running.
  • If you see a port-already-in-use error, another process is using port 8000. Close any other running servers and try again.

help me fix my FastAPI development server not starting

Explore the interactive API documentation

FastAPI automatically generates interactive documentation for every endpoint you create. You can test your API directly from the browser without any extra tools.

  • In your browser, go to http://127.0.0.1:8000/docs.

You should see a page titled FastAPI with your / endpoint listed under a GET heading.

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

You should see a response body showing {"message":"Welcome to the Bookstore API!"} and a response code of 200.

Why is this documentation useful?

Swagger UI lets you test every endpoint without writing any client code. As you add more endpoints in the next steps, they will all appear here automatically with input fields for any parameters they accept.

Your first endpoint is live and responding to requests. Next up, you'll define a data model that describes what a "book" looks like in your API.

Define Your Book Data Model

Your API needs to know the exact shape of data it will accept. When someone sends a book to your API, you need to guarantee it has a title, an author, and a price before storing it.

Pydantic models define the structure of your data and automatically reject anything that doesn't match. In this step, you'll define what a "Book" looks like and create some sample data to work with.

In this step, get ready to:
  • Create a Pydantic model that defines what a Book looks like.
  • Create a response model that adds an ID to each book.
  • Set up sample book data to serve as your in-memory database.
Create a Pydantic Book model

A Pydantic model is a Python class that declares what fields your data must have and what types they should be. FastAPI uses these models to automatically validate incoming requests.

  • In main.py, find this line at the top of the file:
from fastapi import FastAPI
  • Replace it with these two import lines:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

Why import HTTPException and status now?

You won't use HTTPException or status until a later step, but importing them now saves you from editing this line again. BaseModel from pydantic is what you need right now to define your data models.

  • Now add the Book model below the app = FastAPI() line. Leave a blank line between them, then paste this code:
class Book(BaseModel):
    title: str
    author: str
    price: float
    description: str | None = None

What does this code do?

This class declares the exact fields a book must have when someone sends data to your API.

  • title, author, and price are required fields. If a request is missing any of them, FastAPI automatically rejects it with a clear error message.
  • description is optional. The str | None = None syntax means it can be a string or absent entirely.
  • Save main.py.

You won't see a change yet. The next chunk adds the response model and sample data that put this model to use.

Seeing a syntax error?

  • Make sure the from pydantic import BaseModel line is on its own line, directly below the fastapi import.
  • The str | None syntax requires Python 3.10 or higher. If you see a TypeError, check your Python version.
  • Confirm class Book(BaseModel): uses BaseModel with a capital B and capital M.

Why is my Pydantic model causing a syntax error?

Create a BookResponse model and set up sample data

When your API returns a book, it needs to include an ID so the client can reference it later. A separate response model handles this by extending the base Book model with an id field.

You'll also need some books to start with. Instead of setting up a database, you'll use a simple Python list as your in-memory storage.

  • In main.py, add the following code below the Book class you just created. Leave a blank line between the Book class and this new code:
class BookResponse(Book):
    id: int


# In-memory "database"
books: list[dict] = [
    {
        "id": 1,
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "price": 12.99,
        "description": "A story of the fabulously wealthy Jay Gatsby.",
    },
    {
        "id": 2,
        "title": "1984",
        "author": "George Orwell",
        "price": 9.99,
        "description": "A dystopian novel about totalitarianism.",
    },
]

next_id = 3

What does this code do?

  • BookResponse(Book) inherits all the fields from Book and adds an id field. This is what your API returns to clients.
  • books is a plain Python list of dictionaries acting as your database. Each dictionary represents one book with all the fields defined in your models.
  • next_id = 3 tracks the next available ID. Since the two sample books use IDs 1 and 2, the next book added will get ID 3.

Why use a list instead of a real database?

A Python list keeps the focus on learning REST API patterns without spending time on database setup. The trade-off is that your data resets every time the server restarts. For a learning project, that's perfectly fine.

  • Save main.py.
  • If your development server is running, it reloads automatically. Visit http://127.0.0.1:8000 in your browser to confirm the server is still running. You should see the same {"message": "Welcome to the Bookstore API!"} response as before.

Server not reloading or showing an error?

  • Check that the BookResponse class appears below the Book class. Python reads top-to-bottom, so BookResponse(Book) won't work if Book hasn't been defined yet.
  • Make sure the books list and next_id variable sit between the models and the @app.get("/") decorator.
  • If the server crashed, restart it by running fastapi dev main.py in your terminal.

Why is my FastAPI server crashing after adding models and data?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

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

app = FastAPI()


class Book(BaseModel):
    title: str
    author: str
    price: float
    description: str | None = None


class BookResponse(Book):
    id: int


# In-memory "database"
books: list[dict] = [
    {
        "id": 1,
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "price": 12.99,
        "description": "A story of the fabulously wealthy Jay Gatsby.",
    },
    {
        "id": 2,
        "title": "1984",
        "author": "George Orwell",
        "price": 9.99,
        "description": "A dystopian novel about totalitarianism.",
    },
]

next_id = 3


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

Your data models are in place and your sample books are ready. Next up, you'll build the endpoints that let clients read this data through the API.

Build the Read Endpoints

Your bookstore has its data model and sample books ready. The next step is making that data accessible through real API endpoints.

Right now, the books list only exists inside your Python file with no way for clients to request it. GET endpoints solve this by letting clients read data from your server.

In this step, get ready to:
  • Create an endpoint that returns all books in the catalog.
  • Create an endpoint that returns a single book by its ID.
  • Test both endpoints interactively using Swagger UI.
Create the GET /books endpoint

The first endpoint returns the entire catalog as a list. FastAPI uses the @app.get() decorator to define endpoints that respond to GET requests.

  • In main.py, add the get_books function below your existing root function by copying this code:
@app.get("/books", response_model=list[BookResponse])
def get_books():
    return books

What does this code do?

  • @app.get("/books") registers this function to handle GET requests sent to the /books path.
  • response_model=list[BookResponse] tells FastAPI to validate the response as a list of BookResponse objects. This also generates the response schema in your docs automatically.
  • The function returns the books list directly. FastAPI converts it to JSON for you.
  • Save main.py.
  • Switch to your browser and refresh http://127.0.0.1:8000/docs. You should see a new GET /books endpoint listed alongside the existing GET / endpoint.

Don't see the new endpoint?

  • Make sure you saved main.py. The dev server auto-reloads on save, but only if the file is actually written to disk.
  • Check your terminal for error messages. A syntax error (like a missing colon or incorrect indentation) prevents the server from reloading.
  • If the page doesn't load at all, your dev server may have stopped. Run fastapi dev main.py in your terminal to restart it.

Why isn't my new GET /books endpoint showing up in Swagger UI?

Create the GET /books/{book_id} endpoint

Sometimes you only want a single book, not the whole catalog. A path parameter lets you specify which book by including its ID directly in the URL.

  • In main.py, add the get_book function below your get_books function:
@app.get("/books/{book_id}", response_model=BookResponse)
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 path is a path parameter. FastAPI extracts it from the URL and passes it to the function as an argument.
  • book_id: int tells FastAPI to validate that the value is an integer. If someone passes text like /books/abc, FastAPI returns a validation error automatically.
  • The function loops through books looking for a matching ID. If it finds one, it returns that book.
  • If no match is found, raise HTTPException(status_code=404, detail="Book not found") sends a 404 Not Found response back to the client.

What is a 404 status code?

HTTP status codes are numbers that tell the client what happened with their request. A 200 means success. A 404 means the thing they asked for doesn't exist.

You've probably seen 404 pages when clicking a broken link on the web. Your API returns the same code when someone asks for a book ID that isn't in the list.

  • Save main.py.
  • Refresh http://127.0.0.1:8000/docs in your browser. You should see a second new endpoint: GET /books/{book_id}.

Don't see GET /books/{book_id}?

  • Confirm the @app.get("/books/{book_id}") decorator is on the line directly above def get_book(book_id: int):.
  • Check that your curly braces match exactly: {book_id} in the path string. Square brackets or missing braces won't work.
  • Look at your terminal for any traceback. A missing import or typo in HTTPException prevents the server from reloading.

Why isn't my GET /books/{book_id} endpoint appearing in Swagger?

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py before moving on to testing.

ⓧ I'd like to double check the full code

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

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

app = FastAPI()


class Book(BaseModel):
    title: str
    author: str
    price: float
    description: str | None = None


class BookResponse(Book):
    id: int


# In-memory "database"
books: list[dict] = [
    {
        "id": 1,
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "price": 12.99,
        "description": "A story of the fabulously wealthy Jay Gatsby.",
    },
    {
        "id": 2,
        "title": "1984",
        "author": "George Orwell",
        "price": 9.99,
        "description": "A dystopian novel about totalitarianism.",
    },
]

next_id = 3


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


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


@app.get("/books/{book_id}", response_model=BookResponse)
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")
Test both endpoints in Swagger UI

Swagger UI isn't just documentation. You can execute real requests directly from the browser and see the responses instantly.

  • In your browser, go to http://127.0.0.1:8000/docs (refresh if already open).
  • Click on the GET /books endpoint to expand it.
  • Click Try it out.
  • Click Execute.

You should see a JSON response containing both sample books (The Great Gatsby and 1984) with a 200 status code.

  • Now click on the GET /books/{book_id} endpoint to expand it.
  • Click Try it out.
  • Enter 1 in the book_id field.
  • Click Execute.

You should see a JSON response with just The Great Gatsby's details and a 200 status code.

  • Change the book_id value to 99.
  • Click Execute again.

This time you should see a 404 response with {"detail": "Book not found"} in the response body.

Why test with a non-existent ID?

Testing the "unhappy path" (what happens when something goes wrong) is just as important as testing the happy path. Your API should handle bad requests gracefully instead of crashing.

The 404 response tells the client clearly that the book doesn't exist, rather than returning an empty response or an error page.

Your read endpoints are working. Next up, you'll build the endpoints that let clients create, update, and delete books from the catalog.

Build the Create, Update, and Delete Endpoints

Your GET endpoints let users browse the bookstore catalog. Now you need to let them add new books, update existing ones, and remove books they no longer want.

Each of these operations maps to a different HTTP method: POST for creating, PUT for updating, and DELETE for removing. Together with the GET endpoints you already built, these form a complete CRUD API.

In this step, get ready to:
  • Create a POST endpoint that adds new books to the catalog.
  • Create a PUT endpoint that updates an existing book's details.
  • Create a DELETE endpoint that removes a book from the catalog.
Create the POST endpoint for adding books

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

  • In main.py, find the get_book function (your last endpoint). Add the following code below it:
@app.post("/books", response_model=BookResponse, status_code=status.HTTP_201_CREATED)
def create_book(book: Book):
    global next_id
    # Convert the Pydantic model to a dict and add the auto-generated ID
    new_book = {"id": next_id, **book.model_dump()}
    # Add the new book to our in-memory list
    books.append(new_book)
    # Increment the counter so the next book gets a unique ID
    next_id += 1
    return new_book

What does this code do?

The @app.post decorator tells FastAPI this endpoint handles POST requests. The status_code=status.HTTP_201_CREATED sets the response to return a 201 status code, which means "a new resource was created successfully."

The book: Book parameter tells FastAPI to expect a JSON request body matching the Book model. FastAPI validates the incoming data automatically and rejects requests that don't match the model's fields.

The .model_dump() method converts the Pydantic model into a plain Python dictionary. The ** unpacks that dictionary so you can merge it with the id key.

  • Save main.py.
  • Switch to the Swagger UI in your browser at http://127.0.0.1:8000/docs and refresh the page.
  • You should see a new green POST /books endpoint in the list.
  • Click on it, then click Try it out.
  • Replace the example JSON in the request body with this:
{
  "title": "Dune",
  "author": "Frank Herbert",
  "price": 14.99,
  "description": "A science fiction masterpiece about desert planet Arrakis."
}
  • Click Execute.
  • You should see a 201 response with the book data including an id of 3.

Not seeing the POST endpoint?

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

Check your terminal for error messages. A common issue is placing the new function inside another function by accident. The @app.post decorator should be at the same indentation level as the other @app.get decorators.

Help me debug my POST endpoint

Create the PUT endpoint for updating books

The PUT endpoint needs both a path parameter (which book to update) and a request body (the new data). FastAPI automatically figures out which parameter comes from the URL and which comes from the JSON body.

  • In main.py, add the following code below the create_book function:
@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: Book):
    # Loop through books to find the one with the matching ID
    for i, existing_book in enumerate(books):
        if existing_book["id"] == book_id:
            # Replace the old book data with the updated data
            books[i] = {"id": book_id, **book.model_dump()}
            return books[i]
    # If no book matched, return a 404 error
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

This function takes two parameters: book_id: int comes from the URL path, and book: Book comes from the JSON request body. FastAPI matches path parameters by name and treats Pydantic models as request bodies automatically.

The enumerate() function gives you both the index position and the value as you loop. You need the index so you can replace the book at that position in the list.

  • Save main.py and refresh the Swagger UI.
  • You should see a new blue PUT /books/{book_id} endpoint.
  • Click on it, then click Try it out.
  • Enter 1 in the book_id field.
  • Replace the request body JSON with an updated price for The Great Gatsby:
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "price": 15.99,
  "description": "A story of the fabulously wealthy Jay Gatsby."
}
  • Click Execute. You should see a 200 response with the updated book showing the new price of 15.99.

Getting a 422 Unprocessable Entity error?

This means your request body JSON doesn't match the Book model. Make sure all required fields (title, author, price) are present and that price is a number (not a string in quotes).

Help me fix my 422 error

Create the DELETE endpoint for removing books

The DELETE endpoint only needs a path parameter. There is no request body because you are simply telling the server which book to remove.

  • In main.py, add the following code below the update_book function:
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    # Loop through books to find the one with the matching ID
    for i, book in enumerate(books):
        if book["id"] == book_id:
            # Remove the book from the list
            books.pop(i)
            return {"message": "Book deleted successfully"}
    # If no book matched, return a 404 error
    raise HTTPException(status_code=404, detail="Book not found")

What does this code do?

This endpoint doesn't specify a response_model because it returns a simple confirmation message instead of a book object. The books.pop(i) removes the item at index i from the list permanently.

The same 404 error pattern from the GET and PUT endpoints appears here. If someone tries to delete a book that doesn't exist, the API returns a clear error instead of silently doing nothing.

  • Save main.py and refresh the Swagger UI.
  • You should see a new red DELETE /books/{book_id} endpoint.
  • Click on it, then click Try it out.
  • Enter 2 in the book_id field and click Execute.
  • You should see a 200 response with the message "Book deleted successfully".

Verify the delete worked

Try the GET /books endpoint again. You should see that book ID 2 ("1984") is gone from the list. Try deleting book ID 2 a second time and you'll get a 404 error confirming the book no longer exists.

Seeing a 500 Internal Server Error?

Check your terminal for a traceback. A common cause is a typo in the variable name inside the loop. Make sure the loop uses for i, book in enumerate(books) and accesses book["id"] (not books["id"]).

Help me debug my DELETE endpoint

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and that all five endpoints (one root, two GET, one POST, one PUT, one DELETE) show up in the Swagger UI.

ⓧ I'd like to double check the full code

Here is the complete main.py file. Make sure yours matches this exactly.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

app = FastAPI()


class Book(BaseModel):
    title: str
    author: str
    price: float
    description: str | None = None


class BookResponse(Book):
    id: int


# In-memory "database"
books: list[dict] = [
    {
        "id": 1,
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "price": 12.99,
        "description": "A story of the fabulously wealthy Jay Gatsby.",
    },
    {
        "id": 2,
        "title": "1984",
        "author": "George Orwell",
        "price": 9.99,
        "description": "A dystopian novel about totalitarianism.",
    },
]

next_id = 3


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


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


@app.get("/books/{book_id}", response_model=BookResponse)
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", response_model=BookResponse, status_code=status.HTTP_201_CREATED)
def create_book(book: Book):
    global next_id
    new_book = {"id": next_id, **book.model_dump()}
    books.append(new_book)
    next_id += 1
    return new_book


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


@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    for i, book in enumerate(books):
        if book["id"] == book_id:
            books.pop(i)
            return {"message": "Book deleted successfully"}
    raise HTTPException(status_code=404, detail="Book not found")

What is CRUD?

CRUD stands for Create, Read, Update, and Delete. These are the four basic operations for managing data in any application. Your bookstore API now implements all four: POST creates, GET reads, PUT updates, and DELETE removes.

You now have a fully functional REST API with complete CRUD operations. Every web application, mobile backend, and microservice you'll ever work with follows this same pattern of HTTP methods mapping to data operations.

Secret mission

Add Search and Filtering

Your bookstore API works great for browsing the full catalog. But what happens when a customer wants to find all books by a specific author, or only books within their budget? Time to add search and filtering capabilities to your API.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • Your project folder containing main.py and the Python virtual environment.
  • The FastAPI development server (running process).

Keep everything running

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

  • Your FastAPI dev server stays running in the terminal.
  • Your project folder and virtual environment remain on your machine ready for further development.

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.

  • Switch to the terminal where your FastAPI dev server is running.
  • Press Ctrl+C to stop the server.
  • Close VS Code when you're done.
  • To resume later, open VS Code, activate your virtual environment, and run fastapi dev main.py 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 dev server is running.
  • Press Ctrl+C to stop the server.
  • Run deactivate to exit the virtual environment.
  • Navigate to the parent directory (the folder that contains your project folder) by running:
cd ..
  • Delete the project folder. On macOS/Linux, run:
rm -rf bookstore-api

On Windows (PowerShell), run:

Remove-Item -Recurse -Force bookstore-api

Replace the folder name if needed

If you named your project folder something different, replace bookstore-api with the actual folder name you created in Step 1.

  • Confirm deletion by running ls (macOS/Linux) or dir (Windows). Your project folder should no longer appear in the output.

Nice Work!

Nice Work!

You just built a REST API from scratch. You now have a fully functional Bookstore API that handles creating, reading, updating, and deleting books, all tested through an interactive browser-based interface.

You've learned how to:

  • Build a complete CRUD API with FastAPI and Python, using proper HTTP methods (GET, POST, PUT, DELETE) and status codes (200, 201, 404).
  • Define Pydantic data models for automatic request validation and response serialization.
  • Test all endpoints interactively using FastAPI's auto-generated Swagger UI documentation at /docs.
  • Added search and filtering with query parameters so users can find books by author name or price range.

Ready to quiz yourself?