Build a Bookstore Inventory API
Build a REST API with FastAPI and SQLite to manage a bookstore inventory.
Introduction
30 Second Summary
Think about searching for a book online. Behind that search bar is an API that handles every request to look up titles, check stock, and update inventory.
In this project, you will build a REST API for a bookstore's inventory system using Python and FastAPI. You'll create endpoints that let users add books, browse the catalog, update details, and remove items from stock.
What You'll Build
A fully functional REST API with five endpoints that serve JSON data from a SQLite database, complete with auto-generated interactive documentation for testing.
By the end of this project, you'll have:
- A FastAPI application with full CRUD (Create, Read, Update, Delete) endpoints for managing books.
- A SQLite database with a books table and a clean data model using separate input and output schemas.
- Interactive Swagger docs at /docs where you can test every endpoint live in the browser.
- Secret Mission: Add filtering and search so users can find books by genre or search by title.
Are there any prerequisites?
You should be comfortable with basic Python (functions, classes, and variables) and have a general familiarity with SQL concepts like SELECT and INSERT. No prior experience with FastAPI or building APIs is needed.
Set Up Your Project
Every API needs an entry point. This is the file that receives incoming requests and decides what to do with them.
Your virtual environment is ready and your packages are installed. Now you'll create the actual application file and confirm that FastAPI is wired up correctly.
In this step, get ready to:
- Create a main.py file with a FastAPI app instance.
- Verify the app runs successfully with fastapi dev.
Create your FastAPI app file
- In VS Code, look at the Explorer sidebar on the left.
- Hover over your project folder name and click the New File icon (it looks like a page with a small plus sign).
- Name the file main.py and press Enter.
The file opens automatically in the editor. Add the following code to it:
from fastapi import FastAPI
app = FastAPI(title="Bookstore Inventory API")
What does this code do?
- from fastapi import FastAPI brings in the FastAPI class from the package you installed earlier.
- app = FastAPI(title="Bookstore Inventory API") creates an application instance. The title parameter sets the name that appears on the auto-generated documentation page.
- Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and move on to starting the server.
ⓧ 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(title="Bookstore Inventory API")
Start the development server
FastAPI includes a built-in development server that auto-reloads whenever you change your code. This lets you test your API without restarting anything manually.
- In your terminal (make sure your virtual environment is still active), start the server by running this command:
fastapi dev
- You should see output in the terminal ending with a line that includes Uvicorn running on http://127.0.0.1:8000.
- Open your browser and navigate to http://127.0.0.1:8000/docs.
You should see the Swagger UI page with the title Bookstore Inventory API at the top. The page is empty because you haven't added any endpoints yet.
Server not starting?
- Check that your virtual environment is active. You should see (venv) at the start of your terminal prompt. If it's missing, activate it again with source venv/bin/activate (macOS) or venv\Scripts\activate (Windows).
- Make sure your file is named exactly main.py. The fastapi dev command auto-discovers files named main.py or app.py.
- If you see a ModuleNotFoundError, FastAPI may not be installed in your active environment. Run pip install "fastapi[standard]" and try again.
Why isn't my FastAPI dev server starting?
Your API is alive. Next up, you'll design the data model that defines what a book looks like in your system.
Design Your Data Model
Your FastAPI app is running, but it has no idea what a "book" looks like. Before you can store or serve any data, you need a data model that defines the shape of a book in your system.
In this step, you'll use SQLModel to define your data model. SQLModel combines Pydantic (for data validation) with SQLAlchemy (for database access) into a single class definition. One model gives you a database table, automatic validation, and JSON serialization all at once.
In this step, get ready to:
- Define the Book model with fields like title, author, genre, price, and stock.
- Create separate input and output models to control what data goes in and comes out of your API.
- Connect your models to a SQLite database that starts automatically with your app.
Define the Book model with SQLModel
Every book in your inventory has the same set of attributes: a title, an author, a genre, a price, a stock count, and a publication year. Your model class translates these real-world attributes into a database table that SQLModel can read and write.
- In VS Code, open main.py.
- Select all the existing code and replace it with the following:
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Models ---
class BookBase(SQLModel):
title: str = Field(index=True)
author: str = Field(index=True)
genre: str
price: float = Field(ge=0)
stock: int = Field(ge=0)
published_year: int
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
What does this code do?
The imports bring in everything you'll need across this step and the next. You won't use all of them immediately, but having them ready avoids import errors later.
- BookBase is the parent model that defines the shared fields every book has. Field(index=True) tells the database to index that column for faster lookups. Field(ge=0) adds validation so price and stock can never be negative.
- Book inherits from BookBase and adds table=True, which tells SQLModel to create an actual database table from this class. The id field is auto-generated by the database, so it defaults to None until a book is saved.
You won't see a change yet. The next chunk adds the remaining models that complete the data layer.
Create input and output models
A well-designed API never exposes its raw database table directly. You create separate models for what data the API accepts (input) and what it returns (output). This keeps your internal database structure private and your API contract clean.
- In main.py, add the following code below the Book class:
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
class BookUpdate(SQLModel):
title: str | None = None
author: str | None = None
genre: str | None = None
price: float | None = Field(default=None, ge=0)
stock: int | None = Field(default=None, ge=0)
published_year: int | None = None
What do these models do?
- BookCreate is what the API accepts when someone adds a new book. It inherits all fields from BookBase but has no id field because the database assigns the ID automatically.
- BookPublic is what the API returns. It includes the id so clients can reference specific books.
- BookUpdate makes every field optional (None by default). This lets users update just the price or just the stock without sending the entire book object.
You won't see a visible change yet. The next chunk connects these models to a live database.
Set up the database engine and startup event
Your models define the shape of your data, but they need a database engine to actually store it. SQLite is a file-based database that requires zero configuration. FastAPI's startup event will create the database and tables the moment your app launches.
- In main.py, add the following code below the BookUpdate class:
# --- Database ---
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
# --- App ---
app = FastAPI(title="Bookstore Inventory API")
@app.on_event("startup")
def on_startup():
create_db_and_tables()
What does this code do?
- sqlite_url points to a local file called database.db. SQLite creates this file automatically the first time you write to it.
- connect_args={"check_same_thread": False} is required because FastAPI handles requests across multiple threads, but SQLite only allows one thread by default. This setting removes that restriction.
- get_session() is a dependency function that opens a database session for each request and closes it when the request finishes. Your endpoints will use this to read and write data.
- @app.on_event("startup") runs create_db_and_tables() the moment your server starts, ensuring the books table exists before any request arrives.
- Save main.py.
- Check your terminal where fastapi dev is running. You should see the server auto-reload without any errors.
- Confirm the database file was created by running this in a new terminal:
ls database.db
You should see database.db listed in the output. This means your startup event ran successfully and created the SQLite database file.
Server showing an error on reload?
- If you see IndentationError, double-check that each class body is indented with 4 spaces. Mixing tabs and spaces causes this.
- If you see ImportError, make sure your virtual environment is active and that you installed sqlmodel in the previous step.
- If you don't see database.db after ls, make sure your terminal is in the same folder as main.py. Run ls main.py to confirm.
Help me debug my SQLModel setup
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and that your server is running without errors.
ⓧ I'd like to double check the full code
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Models ---
class BookBase(SQLModel):
title: str = Field(index=True)
author: str = Field(index=True)
genre: str
price: float = Field(ge=0)
stock: int = Field(ge=0)
published_year: int
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
class BookUpdate(SQLModel):
title: str | None = None
author: str | None = None
genre: str | None = None
price: float | None = Field(default=None, ge=0)
stock: int | None = Field(default=None, ge=0)
published_year: int | None = None
# --- Database ---
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
# --- App ---
app = FastAPI(title="Bookstore Inventory API")
@app.on_event("startup")
def on_startup():
create_db_and_tables()
Your data layer is locked in. You have models that define what a book looks like, validate incoming data, and control what gets sent back to clients. Next up, you'll build the endpoints that let users actually create and read books through your API.
Build the Create and Read Endpoints
Your data model is ready and the database creates itself on startup. Now it's time to give your API its purpose: letting clients actually interact with bookstore data.
In this step, you'll build the three endpoints that handle creating books and reading them back. These cover the "C" and "R" in CRUD (Create, Read, Update, Delete).
In this step, get ready to:
- Build a POST endpoint that accepts book data and saves it to the database.
- Build a GET endpoint that returns all books from the catalog.
- Build a GET endpoint that looks up a single book by its ID or returns a 404 error.
Build the POST /books/ endpoint
Every bookstore needs a way to add new books to the inventory. The POST endpoint accepts book data from the client, validates it against your BookCreate model, and saves it to the database.
- In main.py, scroll to the bottom of the file (below the on_startup function) and add the following endpoint code:
# --- Endpoints ---
@app.post("/books/", response_model=BookPublic, status_code=201)
def create_book(*, session: Session = Depends(get_session), book: BookCreate):
db_book = Book.model_validate(book)
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
What does this code do?
This endpoint handles incoming POST requests to /books/.
- response_model=BookPublic tells FastAPI to filter the response through your BookPublic schema, so the client always gets a consistent shape (including the auto-generated id).
- status_code=201 returns a "201 Created" status instead of the default 200, which is the correct HTTP convention for creating a new resource.
- Book.model_validate(book) converts the incoming BookCreate data into a full Book database object.
- session.commit() writes the book to the database, and session.refresh(db_book) reloads it so the auto-generated id is populated.
- Save main.py.
- Start the development server by running this command in your terminal:
fastapi dev main.py
- Open your browser and go to http://127.0.0.1:8000/docs.
- Click the POST /books/ section to expand it.
- Click Try it out.
- Replace the example JSON in the request body with this:
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"genre": "Fiction",
"price": 12.99,
"stock": 5,
"published_year": 1925
}
- Click Execute.
- You should see a 201 response with the book data returned, including an id field (likely 1).
What is Swagger UI?
FastAPI automatically generates interactive documentation at /docs based on your endpoint definitions and type hints. You can test every endpoint directly in the browser without needing a separate tool like Postman.
Seeing a validation error?
If you get a 422 response, check that your JSON body includes all required fields (title, author, genre, price, stock, published_year) and that price and stock are not negative numbers.
If the server won't start, make sure your endpoint code is outside the on_startup function at the module level. Help me debug my POST /books/ endpoint
Build the GET /books/ endpoint
Now that you can add books, you need a way to retrieve them. This endpoint returns the full list of books in the catalog with built-in pagination.
- In main.py, add this code directly below your create_book function:
@app.get("/books/", response_model=list[BookPublic])
def read_books(
*,
session: Session = Depends(get_session),
offset: int = 0,
limit: int = Query(default=100, le=100),
):
books = session.exec(select(Book).offset(offset).limit(limit)).all()
return books
What does this code do?
- response_model=list[BookPublic] tells FastAPI the response is a list of books, each shaped by BookPublic.
- offset and limit are query parameters that enable pagination. A client can request /books/?offset=10&limit=5 to skip the first 10 books and get only 5.
- Query(default=100, le=100) caps the maximum page size at 100 to prevent a single request from dumping the entire database.
- Save main.py. The dev server auto-reloads your changes.
- Switch back to the Swagger UI at http://127.0.0.1:8000/docs and refresh the page.
- Expand the GET /books/ section, click Try it out, then click Execute.
- You should see a JSON array containing the book you added in the previous substep.
Getting an empty list?
If you see [] instead of your book, the database may have been recreated. Go back to the POST endpoint and add a book again, then retry the GET.
If the endpoint doesn't appear in Swagger at all, make sure you saved the file and check the terminal for syntax errors. Help me debug my GET /books/ endpoint
Build the GET /books/{book_id} endpoint
Clients often need to look up a specific book by its ID. This endpoint uses a path parameter to grab the ID from the URL and return that single book, or a 404 error if it doesn't exist.
- In main.py, add this code directly below your read_books function:
@app.get("/books/{book_id}", response_model=BookPublic)
def read_book(*, session: Session = Depends(get_session), book_id: int):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
What does this code do?
- {book_id} in the URL path becomes a function parameter. FastAPI automatically converts it to an integer because of the book_id: int type hint.
- session.get(Book, book_id) fetches a single row by primary key. It returns None if no matching book exists.
- HTTPException(status_code=404) sends back a proper 404 Not Found response instead of silently returning empty data. This is the standard way to handle missing resources in a REST API.
- Save main.py and refresh the Swagger UI.
- Expand GET /books/{book_id}, click Try it out, enter 1 for the book_id field, and click Execute.
- You should see the book you created earlier returned as JSON.
- Now try entering 999 for the book_id and click Execute again.
- You should see a 404 response with the message Book not found.
Seeing a 422 instead of 404?
A 422 means the book_id value you entered couldn't be converted to an integer. Make sure you're entering a plain number like 1 or 999, not text.
If you get a 500 error, check that your if not book: check comes before return book. Help me debug my GET /books/{book_id} endpoint
✔️ Awesome, I've got everything!
Great. Make sure you've saved main.py and that all three endpoints appear in your Swagger UI.
ⓧ I'd like to double check the full code
Here's the complete main.py at the end of this step:
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Models ---
class BookBase(SQLModel):
title: str = Field(index=True)
author: str = Field(index=True)
genre: str
price: float = Field(ge=0)
stock: int = Field(ge=0)
published_year: int
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
class BookUpdate(SQLModel):
title: str | None = None
author: str | None = None
genre: str | None = None
price: float | None = Field(default=None, ge=0)
stock: int | None = Field(default=None, ge=0)
published_year: int | None = None
# --- Database ---
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
# --- App ---
app = FastAPI(title="Bookstore Inventory API")
@app.on_event("startup")
def on_startup():
create_db_and_tables()
# --- Endpoints ---
@app.post("/books/", response_model=BookPublic, status_code=201)
def create_book(*, session: Session = Depends(get_session), book: BookCreate):
db_book = Book.model_validate(book)
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
@app.get("/books/", response_model=list[BookPublic])
def read_books(
*,
session: Session = Depends(get_session),
offset: int = 0,
limit: int = Query(default=100, le=100),
):
books = session.exec(select(Book).offset(offset).limit(limit)).all()
return books
@app.get("/books/{book_id}", response_model=BookPublic)
def read_book(*, session: Session = Depends(get_session), book_id: int):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
Your API can now create and read books from the database. Next up, you'll complete the CRUD set by adding update and delete endpoints so the bookstore can manage its inventory over time.
Build the Update and Delete Endpoints
Your API can create books and retrieve them. But a real bookstore needs to change prices, restock shelves, and remove discontinued titles.
In this step, you'll add the remaining two CRUD operations: Update and Delete. These endpoints let you modify existing books without recreating them, and remove books from your inventory entirely.
In this step, get ready to:
- Build a PATCH endpoint for partial updates.
- Build a DELETE endpoint to remove books.
- Test all five endpoints using Swagger UI.
Build the PATCH endpoint for partial updates
When a bookstore updates a book's price or restocks inventory, they don't want to resend every single field. A PATCH endpoint accepts only the fields that changed.
- In main.py, scroll to the bottom of your file (below the read_book function).
- Add the PATCH endpoint by copying this code at the end of the file:
@app.patch("/books/{book_id}", response_model=BookPublic)
def update_book(
*, session: Session = Depends(get_session), book_id: int, book: BookUpdate
):
# Look up the book by ID
db_book = session.get(Book, book_id)
if not db_book:
raise HTTPException(status_code=404, detail="Book not found")
# Only update the fields the client actually sent
book_data = book.model_dump(exclude_unset=True)
db_book.sqlmodel_update(book_data)
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
What does this code do?
This endpoint uses the BookUpdate model you defined in Step 2. Every field in that model is optional (None by default), so the client can send just the fields they want to change.
- model_dump(exclude_unset=True) extracts only the fields the client actually included in the request. If someone sends just {"price": 12.99}, only the price changes.
- sqlmodel_update() applies those changes to the existing database record without touching any other fields.
Why PATCH instead of PUT?
PUT requires the client to send the entire object every time, even if only one field changed. PATCH with exclude_unset=True allows partial updates, which is the pattern most production APIs use.
Imagine updating just a book's stock count. With PUT you'd need to resend the title, author, genre, and every other field. With PATCH, you send {"stock": 50} and you're done.
- Save main.py.
- Your terminal running fastapi dev should auto-reload. Check for any error messages in the terminal output.
Seeing an indentation or syntax error?
- Make sure the @app.patch decorator is at the same indentation level as your other @app.get decorators (no extra indentation).
- Check that there's a blank line between the end of the read_book function and the new @app.patch decorator.
- Confirm the function signature has *, as the first parameter (this forces keyword-only arguments).
Why is my PATCH endpoint causing a syntax error?
Build the DELETE endpoint
The last piece of CRUD is removing books from inventory. The DELETE endpoint follows the same lookup-or-404 pattern, then removes the record from the database.
- In main.py, add the DELETE endpoint below the update_book function you just wrote:
@app.delete("/books/{book_id}")
def delete_book(*, session: Session = Depends(get_session), book_id: int):
# Look up the book or return 404
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
# Remove the book from the database
session.delete(book)
session.commit()
return {"ok": True}
What does this code do?
- session.get(Book, book_id) looks up the book. If it doesn't exist, the endpoint returns a 404 error.
- session.delete(book) removes the record from the database.
- The endpoint returns {"ok": True} as a simple confirmation that the deletion succeeded.
- Save main.py.
- Check your terminal for a successful reload message with no errors.
Server not reloading?
- Make sure fastapi dev is still running in your terminal. If you accidentally closed it, run it again.
- If you see a NameError, check that HTTPException is still in your import statement at the top of the file.
Why isn't my FastAPI dev server reloading after I save?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete main.py with all five endpoints:
from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select
# --- Models ---
class BookBase(SQLModel):
title: str = Field(index=True)
author: str = Field(index=True)
genre: str
price: float = Field(ge=0)
stock: int = Field(ge=0)
published_year: int
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
class BookUpdate(SQLModel):
title: str | None = None
author: str | None = None
genre: str | None = None
price: float | None = Field(default=None, ge=0)
stock: int | None = Field(default=None, ge=0)
published_year: int | None = None
# --- Database ---
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
# --- App ---
app = FastAPI(title="Bookstore Inventory API")
@app.on_event("startup")
def on_startup():
create_db_and_tables()
# --- Endpoints ---
@app.post("/books/", response_model=BookPublic, status_code=201)
def create_book(*, session: Session = Depends(get_session), book: BookCreate):
db_book = Book.model_validate(book)
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
@app.get("/books/", response_model=list[BookPublic])
def read_books(
*,
session: Session = Depends(get_session),
offset: int = 0,
limit: int = Query(default=100, le=100),
):
books = session.exec(select(Book).offset(offset).limit(limit)).all()
return books
@app.get("/books/{book_id}", response_model=BookPublic)
def read_book(*, session: Session = Depends(get_session), book_id: int):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
@app.patch("/books/{book_id}", response_model=BookPublic)
def update_book(
*, session: Session = Depends(get_session), book_id: int, book: BookUpdate
):
db_book = session.get(Book, book_id)
if not db_book:
raise HTTPException(status_code=404, detail="Book not found")
book_data = book.model_dump(exclude_unset=True)
db_book.sqlmodel_update(book_data)
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
@app.delete("/books/{book_id}")
def delete_book(*, session: Session = Depends(get_session), book_id: int):
book = session.get(Book, book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
session.delete(book)
session.commit()
return {"ok": True}
Test all endpoints using Swagger docs
Your API now has all five CRUD endpoints. The interactive Swagger UI lets you test each one directly in your browser without needing any extra tools.
- Open your browser and go to http://127.0.0.1:8000/docs.
- You should now see all five endpoints listed: POST, two GETs, PATCH, and DELETE.
Try testing the PATCH endpoint to update a book's price.
- Click the PATCH /books/{book_id} row to expand it.
- Click Try it out.
- Enter 1 in the book_id field.
- In the request body, replace the default JSON with:
{
"price": 14.99
}
- Click Execute.
- You should see a 200 response with the full book object, now showing the updated price of 14.99 while all other fields remain unchanged.
Now test the DELETE endpoint.
- Click the DELETE /books/{book_id} row to expand it.
- Click Try it out.
- Enter 1 in the book_id field.
- Click Execute.
- You should see a 200 response with {"ok": true}.
Try requesting the same book again to confirm it's gone.
- Expand the GET /books/{book_id} endpoint.
- Click Try it out, enter 1 for the book_id, and click Execute.
- You should see a 404 response with {"detail": "Book not found"}. This confirms the book was deleted.
What are HTTP status codes telling you?
- 201 (Created): The POST endpoint returns this when a new book is successfully added.
- 200 (OK): PATCH, DELETE, and GET return this for successful operations.
- 404 (Not Found): Any endpoint returns this when the requested book ID doesn't exist.
- 422 (Unprocessable Entity): FastAPI returns this automatically when request data fails validation (e.g., a negative price).
Getting a 404 when you expected a book?
- If you deleted a book earlier and are trying to update it, it's gone. Use POST to create a new one first, then note its new ID.
- If you restarted your server and see no books, check that database.db still exists in your project folder. Your data persists between restarts as long as the file is there.
Help me debug why my endpoint is returning 404 unexpectedly.
Your API is fully functional with all five CRUD endpoints. Next up, you'll take on the Secret Mission and add filtering and search to make this API truly useful for browsing a bookstore catalog.
Secret mission
Add Filtering and Search
Your bookstore API can list all books, but real users need to find specific ones. Can you add query parameters that let users filter by genre, search by title, and narrow results by price range?
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 folder as-is or delete it entirely.
Resources you used:
- Your project folder on the Desktop (contains main.py, the venv virtual environment, and database.db).
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend your API further.
- Your project folder stays on your Desktop.
- The database.db file is tiny (a few kilobytes) and costs nothing to keep.
- The virtual environment stays ready for next time you run fastapi dev.
Pause - I'll come back to this later
Stop the running dev server to free up the terminal. Your files stay on disk for when you return.
- Press Ctrl+C in the terminal where fastapi dev is running to stop the server.
- Deactivate the virtual environment by running:
deactivate
- When you return, navigate back to your project folder, activate the virtual environment again, and run fastapi dev to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything including your database and virtual environment.
- Press Ctrl+C in the terminal to stop the dev server if it's still running.
- Deactivate the virtual environment by running:
deactivate
- Move to your Desktop by running:
cd ~/Desktop
- Delete the project folder by running this command (macOS/Linux):
rm -rf bookstore-api
- On Windows (PowerShell), run this instead:
Remove-Item -Recurse -Force bookstore-api
- Confirm the folder is gone by running ls. You should no longer see bookstore-api in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-style REST API for a bookstore inventory using FastAPI and Python.
You've learned how to:
- Built a complete REST API with 5 CRUD endpoints using FastAPI and Python.
- Designed a data model with separate input and output schemas for clean API contracts.
- Connected your API to a SQLite database using SQLModel with proper error handling and meaningful HTTP status codes.
- Secret Mission: Added filtering and search with query parameters for a real-world catalog experience.
Ready to quiz yourself?