Build a Book Collection REST API
Build a REST API with FastAPI to manage a book collection using full CRUD.
Introduction
30 Second Summary
Every time you open a weather app, search for a product online, or check your bank balance, your device is talking to a server through an API. APIs are the invisible bridges that connect the apps you use every day to the data they need.
In this project, you will build a REST API from scratch using Python and FastAPI. You'll create a server that manages a collection of books, handling everything from adding new books to searching your library.
What You'll Build
A Book Collection API server that runs locally and handles full CRUD operations (Create, Read, Update, Delete) on a personal library of books, with auto-generated interactive documentation.
By the end of this project, you'll have:
- A running FastAPI server with 5 endpoints covering all CRUD operations on your book collection.
- Auto-generated interactive Swagger UI documentation for testing every endpoint directly in your browser.
- A Pydantic data model that validates incoming book data automatically.
- Secret Mission: Add search and filtering to let users find books by title or filter by read status.
Are there any prerequisites?
You'll need basic Python knowledge, including functions, classes, and type hints. No prior experience with APIs or web frameworks is required.
Set Up Your Environment
Before you can build an API, you need the right tools installed and a clean workspace to work in.
In this step, you'll confirm Python is ready, create a dedicated project folder, and install FastAPI so you have everything you need to start coding.
In this step, get ready to:
- Verify Python 3.10 or higher is installed.
- Create a project folder with a virtual environment.
- Install FastAPI and its development tools.
Verify Python is installed
FastAPI requires Python 3.10 or higher. Let's confirm your machine has the right version.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your Python version by running this command:
python3 --version
✔️ I see version 3.10 or higher
Nice, Python is ready to go. Move on to the next substep.
ⓧ I see an older version
Your system has Python installed, but it is an older version. You need 3.10 or higher for this project.
On macOS, the system may ship with an older Python that shadows newer installs. Download and install a recent version from the official Python website.
- Visit python.org/downloads and download the latest Python 3 installer for your platform.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version again to confirm the new version.
macOS version shadowing
If python3 --version still shows the old version after installing, try running the full path (e.g. /usr/local/bin/python3 --version) or use a version manager like pyenv to switch your active Python.
ⓧ Command not found
Python is not installed on your machine yet. Let's fix that.
- Visit python.org/downloads and download the latest Python 3 installer for your platform.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal, then run python3 --version to confirm the installation succeeded.
You should see a version number of 3.10 or higher.
Create your project folder and virtual environment
A virtual environment keeps your project's packages separate from your system Python. This prevents version conflicts between projects.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir book-api
cd book-api
- Create a virtual environment inside book-api by running this command:
python3 -m venv venv
What does this command do?
python3 -m venv venv tells Python to create a new virtual environment in a folder called venv. This folder contains its own copy of Python and a clean space for installing packages.
- Confirm the virtual environment was created by running:
ls
You should see a venv folder in the output.
- Activate the virtual environment by running this command:
macOS
source venv/bin/activate
Windows
venv\Scripts\Activate.ps1
Seeing a script execution error?
PowerShell may block scripts by default. Run the following command to allow activation, then try the activate command again:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activation command from inside the book-api folder. Run pwd (macOS) or cd (Windows) to check your current directory.
If you're in the right place, try closing your terminal and reopening it, then navigate back to book-api and rerun the activation command.
Help me troubleshoot my virtual environment activation
Install FastAPI
With your virtual environment active, you can now install FastAPI and all the tools it needs to run a development server.
- Install FastAPI by running this command:
pip install "fastapi[standard]"
What does this command do?
pip install "fastapi[standard]" installs FastAPI plus a bundle of recommended extras, including a development server with auto-reload.
The quotes around "fastapi[standard]" are required on macOS because the default shell (zsh) would otherwise try to interpret the square brackets.
You'll see several packages download and install. This may take 30 seconds or so.
- Verify the installation succeeded by running:
pip show fastapi
You should see output that includes a Name: fastapi line and a Version: line confirming the package is installed.
Install failed or command not found?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, go back and run the activation command from the previous substep.
If pip is not found, try using pip3 instead, or run python3 -m pip install "fastapi[standard]" as an alternative.
Help me troubleshoot my FastAPI installation
Your development environment is locked and loaded. Next up, you'll write your first API endpoint and see it respond in your browser.
Create Your First API Endpoint
Your environment is set up and FastAPI is installed. Now it's time to write some code and see your API come to life.
In this step, you'll create a minimal API server, start it with one command, and explore the interactive documentation that FastAPI generates for you automatically.
In this step, get ready to:
- Create a main.py file with a minimal FastAPI app.
- Run the development server and see your API respond.
- Explore the auto-generated interactive documentation.
Create your first FastAPI app
A FastAPI app needs three things: an import, an app instance, and at least one route. A route is a Python function decorated with an HTTP method that tells FastAPI what to do when someone visits a specific URL.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Cursor (or Visual Studio Code) and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Select the book-api folder on your Desktop, then click Open.
- Create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
- Save the file immediately by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Name the file main.py and click Save.
- Add the following code to main.py:
# Import the FastAPI framework
from fastapi import FastAPI
# Create the app instance that will handle all requests
app = FastAPI()
# Define a route at the root URL ("/")
@app.get("/")
def read_root():
return {"message": "Welcome to the Book Collection API"}
What does this code do?
- from fastapi import FastAPI imports the FastAPI class from the framework.
- app = FastAPI() creates your application instance. This object is the core of your API.
- @app.get("/") is a decorator that registers the function below it as a handler for GET requests to the root URL /.
- The function read_root() returns a Python dictionary, which FastAPI automatically converts to JSON.
- Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
You won't see a visible change yet. The next substep starts the server so you can see this code in action.
File not saving as .py?
Make sure you typed main.py including the .py extension when naming the file. If it saved without an extension, rename it in the sidebar by right-clicking and selecting Rename.
help me rename my file to main.py
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Welcome to the Book Collection API"}
Run the development server
FastAPI includes a built-in development server that auto-detects your main.py file and starts serving your API. It also watches for code changes and reloads automatically.
- Switch back to your terminal (the one open in the book-api folder with the virtual environment active).
- Start the development server by running this command:
fastapi dev
What should I see?
Your terminal displays a box showing the server URL and documentation URL. Look for a line that says:
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
This means your server is running and ready to receive requests. The terminal will stay occupied with the running server process.
- Open your web browser.
- Type http://127.0.0.1:8000 in the address bar and press Enter.
You should see a JSON response displayed in the browser: {"message":"Welcome to the Book Collection API"}.
- Now visit http://127.0.0.1:8000/docs in your browser.
You should see an interactive documentation page titled FastAPI - Swagger UI. This is auto-generated from your code.
Seeing an error in the terminal?
- Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate (macOS) or venv\Scripts\activate (Windows).
- Check that you are in the book-api folder where main.py lives. Run ls and confirm you see main.py in the output.
- If you see a syntax error, open main.py and compare your code against the double-check tab above.
my fastapi dev command is showing an error
Explore the Swagger UI docs
Swagger UI is an interactive page that documents every endpoint in your API. You can send real requests directly from the browser without any extra tools.
- On the /docs page, find the green GET bar labeled /.
- Click on it to expand the endpoint details.
- Click the Try it out button on the right.
- Click Execute.
You should see a Response body section appear below with your JSON message. You also see the Curl command that was used and the HTTP status code 200 (meaning success).
Why is this useful?
Most API frameworks require you to install separate tools to test your endpoints. FastAPI generates this documentation page for free from your Python type hints and decorators.
As you add more endpoints in the next steps, they'll automatically appear here. This page becomes your testing playground for the entire project.
Your first endpoint is live and responding. Next up, you'll define a data model for books and create endpoints to browse your collection.
Define Your Book Data Model
Your server is running and responding with a welcome message. Now it needs something meaningful to serve.
Every API manages data with a defined shape. In this step, you'll define what a "book" looks like in your system, stock your collection with sample data, and build endpoints that let anyone retrieve books from your API.
In this step, get ready to:
- Define a Book data model using Pydantic.
- Seed your collection with sample books.
- Create endpoints to retrieve books from your API.
Add the Book data model
Before your API can serve book data, you need to define what a "book" looks like. Pydantic's BaseModel lets you declare the shape of your data using Python type hints.
- In main.py, find the line from fastapi import FastAPI at the top of your file.
- Replace everything from that import line down to (but not including) the @app.get("/") decorator with the following code:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# Define the shape of a book using Pydantic
class Book(BaseModel):
id: int
title: str
author: str
read: bool = False
What does this code do?
- from fastapi import FastAPI, HTTPException adds HTTPException to your import. You'll use this later to return error responses.
- from pydantic import BaseModel imports the base class for defining data models.
- The Book class declares that every book has an id, title, author, and read field. The read field defaults to False if not provided.
- Save main.py.
- Check your terminal where the server is running. You should see it reload automatically with no errors.
Seeing an error in the terminal?
- Make sure from pydantic import BaseModel is on its own line directly below the FastAPI import.
- Check that class Book(BaseModel): uses a capital B in both Book and BaseModel.
- Confirm each field inside the class is indented with 4 spaces.
Why isn't my server reloading?
Now you need some books to serve. You'll create an in-memory list seeded with sample data so you can test your endpoints immediately.
- In main.py, add the following code directly below the Book class (above the @app.get("/") decorator):
# Seed the collection with sample books
books: list[Book] = [
Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", read=True),
Book(id=2, title="To Kill a Mockingbird", author="Harper Lee", read=False),
Book(id=3, title="1984", author="George Orwell", read=True),
]
What does this code do?
This creates a Python list called books that holds Book objects. The type annotation list[Book] tells FastAPI (and your editor) exactly what kind of data the list contains.
The three sample books give you real data to test with right away. Since this list lives in memory, it resets every time the server restarts.
- Save main.py. The server should reload again without errors.
Create a GET endpoint for all books
With your data model and sample books in place, you can create an endpoint that returns the full collection as JSON.
- In main.py, add the following code below the existing read_root function:
# Return the full list of books
@app.get("/books")
def get_books():
return books
What does this code do?
- @app.get("/books") registers a new route that responds to GET requests at the /books path.
- The function simply returns the books list. FastAPI automatically converts your Python objects into a JSON response.
- Save main.py.
- Switch back to the Swagger UI at http://127.0.0.1:8000/docs and refresh the page.
- You should see a new GET /books endpoint listed.
- Click on GET /books, then click Try it out, then click Execute.
- You should see a JSON array containing your three sample books in the response body.
Don't see the new endpoint?
- Make sure you saved main.py after adding the new function.
- Refresh the docs page in your browser. The server auto-reloads your code, but the browser needs a manual refresh to pick up new endpoints.
- Check that the get_books function is outside and below the read_root function, not nested inside it.
Why isn't my /books endpoint showing up in the docs?
Create a GET endpoint for a single book
Retrieving the full list is useful, but often you need a specific book. A path parameter lets you pass a book's ID directly in the URL.
- In main.py, add the following code below the get_books function:
# Return a single book by its ID, or 404 if not found
@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 path is a path parameter. FastAPI extracts it from the URL and passes it to your function.
- book_id: int tells FastAPI to automatically convert the URL string to an integer. If someone passes a non-integer, FastAPI returns a validation error automatically.
- The loop searches for a matching book. If found, it returns the book as JSON.
- raise HTTPException(status_code=404, detail="Book not found") sends a 404 response if no book matches the given ID.
- Save main.py and refresh the Swagger UI docs page.
- You should see a new GET /books/{book_id} endpoint listed.
- Click on GET /books/{book_id}, then click Try it out.
- Enter 1 in the book_id field and click Execute.
- You should see The Great Gatsby returned as JSON.
- Now try entering 99 in the book_id field and click Execute again.
- You should see a 404 response with the detail Book not found.
Why return a 404?
HTTP status codes tell the client what happened. A 200 means success. A 404 means the resource doesn't exist. Returning the right status code makes your API predictable for anyone consuming it.
Getting an unexpected error?
- If you see a 422 response, make sure you entered a number (not text) in the book_id field.
- Check that HTTPException is included in your import line at the top: from fastapi import FastAPI, HTTPException.
- Confirm the function parameter name book_id matches the path parameter name in /books/{book_id} exactly.
Why am I getting a 422 or 500 error from my /books/{book_id} endpoint?
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and your server reloaded without errors.
ⓧ I'd like to double check the full code
Here's the complete main.py at this point:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Book(BaseModel):
id: int
title: str
author: str
read: bool = False
books: list[Book] = [
Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", read=True),
Book(id=2, title="To Kill a Mockingbird", author="Harper Lee", read=False),
Book(id=3, title="1984", author="George Orwell", read=True),
]
@app.get("/")
def read_root():
return {"message": "Welcome to the Book Collection API"}
@app.get("/books")
def get_books():
return books
@app.get("/books/{book_id}")
def get_book(book_id: int):
for book in books:
if book.id == book_id:
return book
raise HTTPException(status_code=404, detail="Book not found")
Your API now serves real data and handles missing resources gracefully. Next up, you'll add the ability to create, update, and delete books.
Add Create, Update, and Delete Operations
Your API can read data from the book collection. Clients can browse the full list or look up a single book by its ID.
A read-only API is only half the picture. In this step, you'll add the ability to create new books, update existing ones, and delete books you no longer want. These three operations complete the CRUD pattern (Create, Read, Update, Delete) that nearly every API follows.
In this step, get ready to:
- Define a new Pydantic model for book input data and create a POST endpoint.
- Build a PUT endpoint to update existing books.
- Build a DELETE endpoint to remove books from the collection.
Create the POST endpoint for adding books
When a client creates a new book, they should only need to provide the title, author, and read status. The server is responsible for generating the book's ID. This means you need a separate input model that excludes the id field.
- In main.py, find the Book class. Add the following BookCreate model directly below it (above the books list):
# Input model for creating/updating books (no id field)
class BookCreate(BaseModel):
title: str
author: str
read: bool = False
Why a separate model?
The Book model includes an id field because every stored book has one. But when a client sends data to create a book, they don't know the ID yet. BookCreate only requires the fields the client controls.
This input-vs-output schema pattern is standard in production APIs. It prevents clients from accidentally setting or overriding server-managed fields.
You won't see a change in the browser yet. The next code block uses this model in a new endpoint.
- Now scroll to the bottom of main.py (below the get_book function) and add the POST endpoint:
@app.post("/books", status_code=201)
def create_book(book_data: BookCreate):
# Generate a new ID by finding the current max and adding 1
new_id = max(book.id for book in books) + 1 if books else 1
# Create a full Book instance from the input data plus the generated ID
new_book = Book(id=new_id, **book_data.model_dump())
books.append(new_book)
return new_book
What does this code do?
The @app.post decorator registers this function for HTTP POST requests. The status_code=201 tells FastAPI to return a 201 Created response instead of the default 200.
FastAPI sees the book_data: BookCreate parameter and knows to read the request body as JSON, validate it against the BookCreate model, and reject invalid input automatically.
The .model_dump() method converts the Pydantic model to a dictionary, and ** unpacks it as keyword arguments to create the full Book object with the generated ID.
- Save main.py.
- Switch to your browser and refresh the Swagger UI at http://127.0.0.1:8000/docs. You should see a new green POST /books entry in the endpoint list.
Don't see the new endpoint?
Make sure you saved the file. The dev server auto-reloads on save, but unsaved changes won't appear.
Check that BookCreate is defined above the books list (not below it). Python reads top to bottom, so the model must exist before any function references it.
Help me debug why my POST endpoint isn't showing up in the docs.
Create the PUT endpoint for updating books
Updating a book combines two things you've already seen: a path parameter to identify which book to change, and a request body with the new data.
- In main.py, add the following PUT endpoint below the create_book function:
@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:
# Replace the old book with updated data, keeping the same 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")
What does this code do?
The function accepts both a book_id path parameter (from the URL) and a book_data request body (from the JSON payload). FastAPI figures out which is which based on where each parameter name appears.
The enumerate() loop gives you both the list index and the book object. When a match is found, the function creates a brand-new Book with the original ID and the updated fields, then swaps it into the list at the same position.
- Save main.py.
- Refresh the Swagger UI. You should see a new blue PUT /books/{book_id} entry in the endpoint list.
Seeing a server error in the terminal?
Check that your raise HTTPException line is outside the for loop (at the same indentation as the for keyword). If it's inside the loop, the function raises a 404 on the first non-matching book instead of checking all of them.
Help me fix an indentation error in my FastAPI PUT endpoint.
Create the DELETE endpoint for removing books
The final CRUD operation removes a book entirely from the collection. The pattern is similar to the PUT endpoint, but instead of replacing the book, you remove it from the list.
- In main.py, add the following DELETE endpoint below the update_book function:
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
for index, book in enumerate(books):
if book.id == book_id:
# Remove the book and return a confirmation message
books.pop(index)
return {"message": f"Book '{book.title}' deleted successfully"}
raise HTTPException(status_code=404, detail="Book not found")
What does this code do?
The DELETE endpoint doesn't need a request body. It only needs the book_id from the URL to know which book to remove.
The books.pop(index) call removes the book at that position from the list. The function then returns a JSON message confirming the deletion, including the book's title so the client knows exactly what was removed.
- Save main.py.
- Refresh the Swagger UI. You should now see five endpoints total: GET /, GET /books, GET /books/{book_id}, POST /books, PUT /books/{book_id}, and DELETE /books/{book_id}.
Don't see all five endpoints?
Make sure each endpoint function is at the top level of your file (not indented inside another function or class). Each @app decorator should start at column 0.
If the server crashed, check your terminal for a traceback pointing to the exact line with the syntax error.
Help me figure out why my FastAPI endpoints aren't all showing up.
Why does in-memory data reset?
Every time you save main.py, the dev server restarts and reloads the file from scratch. This means any books you created or deleted through the API are gone, and the original three sample books come back.
This is expected for in-memory storage. A production API would use a database to persist data across restarts.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and all five endpoints appear in the Swagger UI.
ⓧ I'd like to double check the full code
Here's the complete main.py at this point:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Book(BaseModel):
id: int
title: str
author: str
read: bool = False
class BookCreate(BaseModel):
title: str
author: str
read: bool = False
books: list[Book] = [
Book(id=1, title="The Great Gatsby", author="F. Scott Fitzgerald", read=True),
Book(id=2, title="To Kill a Mockingbird", author="Harper Lee", read=False),
Book(id=3, title="1984", author="George Orwell", read=True),
]
@app.get("/")
def read_root():
return {"message": "Welcome to the Book Collection API"}
@app.get("/books")
def get_books():
return books
@app.get("/books/{book_id}")
def get_book(book_id: int):
for book in books:
if book.id == book_id:
return book
raise HTTPException(status_code=404, detail="Book not found")
@app.post("/books", status_code=201)
def create_book(book_data: BookCreate):
new_id = max(book.id for book in books) + 1 if books else 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}")
def delete_book(book_id: int):
for index, book in enumerate(books):
if book.id == book_id:
books.pop(index)
return {"message": f"Book '{book.title}' deleted successfully"}
raise HTTPException(status_code=404, detail="Book not found")
Your API now supports the full CRUD lifecycle. Next up, you'll put all five endpoints through their paces by testing the complete create-read-update-delete flow.
Test Your Complete API
Your API now has all five endpoints covering the full range of CRUD operations. You can create, read, update, and delete books.
In this step, you'll put your API through its paces by testing every endpoint in two different ways. First, you'll use the interactive Swagger UI docs that FastAPI auto-generates. Then you'll use curl from the terminal to see how raw HTTP requests work under the hood.
In this step, get ready to:
- Test the full CRUD lifecycle using Swagger UI.
- Send HTTP requests from the terminal with curl.
- Trigger validation errors by sending invalid data.
Test the full CRUD lifecycle in Swagger UI
The Swagger UI at /docs gives you an interactive playground for every endpoint. You'll walk through the entire lifecycle of a book: creating it, viewing it, updating it, and deleting it.
- Return to your browser and visit http://127.0.0.1:8000/docs.
- Click on the POST /books endpoint to expand it.
- Click Try it out.
- Replace the example JSON in the request body with:
{
"title": "Dune",
"author": "Frank Herbert"
}
- Click Execute.
You should see a response with status code 201 and a JSON body showing your new book with an auto-assigned id of 4.
Why 201 and not 200?
A 201 Created status code specifically tells the client that a new resource was successfully created. The generic 200 OK just means the request succeeded. Using precise status codes helps API consumers understand exactly what happened.
- Now expand the GET /books endpoint, click Try it out, then Execute.
You should see all four books in the response, including the "Dune" entry you just created.
- Expand the GET /books/{book_id} endpoint, click Try it out, enter 4 in the book_id field, then click Execute.
You should see just the "Dune" book object returned on its own.
- Expand the PUT /books/{book_id} endpoint, click Try it out, enter 4 in the book_id field, and use this request body:
{
"title": "Dune Messiah",
"author": "Frank Herbert",
"read": true
}
- Click Execute.
You should see the updated book returned with the new title "Dune Messiah" and read set to true.
- Expand the DELETE /books/{book_id} endpoint, click Try it out, enter 4 in the book_id field, then click Execute.
You should see a confirmation message: Book 'Dune Messiah' deleted successfully.
What just happened?
You just ran through a complete CRUD lifecycle. You created a resource (POST), verified it existed (GET), changed it (PUT), and removed it (DELETE). This is the fundamental pattern behind almost every web application.
Seeing a 404 on GET or DELETE?
If the server auto-reloaded between requests (you'll see a restart message in your terminal), the in-memory data resets. Your created book disappears. Just run the POST step again to re-create it before testing GET, PUT, or DELETE on that ID.
help me understand why my book disappeared after the server restarted
Test your API with curl from the terminal
Swagger UI is great for exploring your API, but real-world clients talk to APIs using raw HTTP requests. The curl command lets you send HTTP requests directly from your terminal, just like another application would.
- Open a second terminal window. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
Why a second terminal?
Your first terminal is still running the FastAPI dev server. You need a separate terminal to send requests to it. The server stays running in one window while you act as the "client" in the other.
- Fetch all books by running this command:
curl http://127.0.0.1:8000/books
You should see a JSON array containing all three seeded books printed in your terminal.
- Create a new book by sending a POST request with a JSON body:
curl -X POST http://127.0.0.1:8000/books -H "Content-Type: application/json" -d '{"title": "1984", "author": "George Orwell"}'
You should see the newly created book returned as JSON, with an auto-assigned id and read defaulting to false.
What do these curl flags mean?
- -X POST tells curl to use the POST HTTP method instead of the default GET.
- -H "Content-Type: application/json" adds a header telling the server the request body is JSON.
- -d '{...}' is the data (request body) you're sending.
- Fetch a single book by ID by running:
curl http://127.0.0.1:8000/books/1
You should see just "The Great Gatsby" book returned as a single JSON object.
Seeing 'command not found' for curl?
On macOS, curl comes pre-installed. On Windows, curl is available in Command Prompt and PowerShell on Windows 10 and later. If you don't have it, you can use the Swagger UI at /docs for all testing instead.
help me test my FastAPI endpoints without curl
Trigger a validation error with invalid data
One of the most powerful features of FastAPI is automatic data validation. Your BookCreate model requires a title field (it's a str with no default). If a client forgets to include it, FastAPI rejects the request with a clear error.
- Send a POST request with a missing title field by running:
curl -X POST http://127.0.0.1:8000/books -H "Content-Type: application/json" -d '{"author": "Unknown"}'
You should see a 422 status response with a JSON body containing a detail array that pinpoints exactly what went wrong.
What does a 422 error look like?
FastAPI returns a structured error response with a detail list. Each item includes loc (where the error is), msg (a human-readable message), and type (the error category). This tells the client exactly which field is missing and why.
This is Pydantic validation in action. You didn't write any validation logic yourself. FastAPI inspected your BookCreate model's type annotations and enforced them automatically.
- Try one more test. Send a request with no JSON body at all:
curl -X POST http://127.0.0.1:8000/books -H "Content-Type: application/json" -d '{}'
You should see a 422 response listing both title and author as missing required fields.
Test 404 errors too
Try fetching a book that doesn't exist: curl http://127.0.0.1:8000/books/99. You'll get a 404 with the message Book not found. That's the HTTPException you wrote in your endpoint code.
You've now confirmed that every endpoint works as expected and that your API gracefully handles both valid and invalid requests. Next up, the Secret Mission will push your API further with search and filtering.
Secret mission
Add Search and Filtering
Your book collection works great, but what happens when it grows to hundreds of books? Right now, GET /books returns everything. In this extension, you'll add search and filtering so users can find exactly what they're looking for.
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 in your terminal.
- The book-api project folder on your Desktop (contains main.py and the venv virtual environment).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your API.
- Your FastAPI server stays running in the terminal.
- You can keep testing endpoints at http://127.0.0.1:8000/docs.
- Your project files remain in the book-api folder on your Desktop.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your project files stay on your machine so you can pick up where you left off.
- In the terminal where your FastAPI server is running, press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
When you're ready to come back, navigate to your book-api folder, reactivate the virtual environment with source venv/bin/activate, and start the server again with fastapi dev.
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 FastAPI server is running, press Ctrl+C to stop the server.
- Deactivate your virtual environment by running:
deactivate
- Navigate to your Desktop and delete the project folder.
macOS
cd ~/Desktop
rm -rf book-api
Windows
cd ~\Desktop
rmdir /s /q book-api
That's it. Python itself stays installed on your machine since you had it before this project.
Nice Work!
Nice Work!
You just built a fully functional REST API from scratch using Python and FastAPI. You now have a working server that manages a book collection, validates data automatically, and comes with beautiful interactive documentation.
You've learned how to:
- Built a REST API server with all CRUD operations (Create, Read, Update, Delete) mapped to the correct HTTP methods.
- Used Pydantic models for automatic data validation and tested endpoints with both Swagger UI docs and curl from the terminal.
- Understood how HTTP status codes communicate success and failure between client and server.
- Added search and filtering with query parameters to let users find books by title or read status.
Ready to quiz yourself?