Build a Bookmark Manager API
Build a REST API with full CRUD operations using Python and FastAPI.
Introduction
30 Second Summary
You save links all the time. Tabs pile up, bookmark folders get buried, and that perfect article you found last week vanishes into the void.
In this project, you will build a Bookmark Manager API using Python and FastAPI. You'll create a backend service that accepts HTTP requests to save, retrieve, update, and delete bookmarks, all running locally on your machine.
What You'll Build
You'll have a working API you can demo right in your browser, sending requests through FastAPI's interactive documentation page and watching structured JSON responses come back instantly.
By the end of this project, you'll have:
- Create bookmarks by sending a URL and title, and get back a structured JSON response with an auto-generated ID and timestamp. List your entire collection or filter by tag to find exactly what you need.
- Update or delete bookmarks by ID, with clear error messages when a bookmark doesn't exist.
- Restart your server anytime and find all your bookmarks still there, persisted to a local JSON file that survives crashes and reboots.
- Secret Mission: Add full-text search across bookmark titles and URLs, plus a stats endpoint showing your collection summary.
Are there any prerequisites?
You'll need basic Python knowledge (variables, functions, dictionaries, and lists). No web development experience is required.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up FastAPI and Run Your First Server
To build your Bookmark Manager API, you need a working development environment first. FastAPI needs to be installed, and you need a project folder with an isolated Python environment to keep your dependencies organized.
In this step, you'll create a project folder, install FastAPI, and spin up a development server that auto-reloads whenever you change your code. You'll also meet FastAPI's built-in interactive documentation page, which is how you'll test your API throughout this project.
In this step, get ready to:
- Create a project folder and Python virtual environment.
- Install FastAPI and create a starter application.
- Run the development server and explore the interactive docs.
Create a project directory and set up a virtual environment
A virtual environment is an isolated space where Python packages are installed without affecting the rest of your system. Every Python project should have its own.
- 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.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir bookmark-api
cd bookmark-api
- Confirm the folder exists by running:
ls
You should see the bookmark-api folder listed in the output.
- Open the bookmark-api folder in your code editor.
- Create a Python virtual environment by running this command:
python3 -m venv .venv
- Activate the virtual environment by running the activation command for your operating system:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
You should see (.venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
- Install FastAPI with all its standard dependencies by running:
pip install "fastapi[standard]"
You should see pip downloading and installing several packages. The output ends with a "Successfully installed" message listing all the packages.
Seeing a permission error or command not found?
- Make sure you see (.venv) in your terminal prompt. If not, re-run the activation command above.
- On macOS, try pip3 instead of pip if you get "command not found".
- On Windows, if PowerShell blocks the activation script, run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser first, then try activating again.
help me fix my FastAPI installation error
Why FastAPI over Flask or Django?
FastAPI generates interactive API documentation automatically from your code. You can test every endpoint from your browser without installing extra tools like Postman.
It also validates incoming data for you using Pydantic models. Flask requires you to set all of this up manually, and Django is built for full websites rather than focused APIs.
Create the starter application
- In your code editor, create a new file called main.py in the bookmark-api folder.
- Copy the following code into main.py:
from fastapi import FastAPI
# Create the FastAPI application
app = FastAPI(title="Bookmark Manager API")
# Root endpoint that returns a welcome message
@app.get("/")
def root():
return {"message": "Bookmark Manager API", "docs": "/docs"}
What does this code do?
- from fastapi import FastAPI imports the FastAPI class that powers your entire application.
- app = FastAPI(title="Bookmark Manager API") creates the application instance. The title appears in the auto-generated docs page.
- @app.get("/") is a decorator that registers the function below it as a handler for GET requests to the root URL.
- The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
- Save main.py.
- Start the development server by running this command in your terminal:
fastapi dev main.py
What does this command do?
fastapi dev starts a local development server using Uvicorn with auto-reload enabled. Whenever you save changes to your code, the server automatically restarts to pick them up.
This command takes over your terminal window. Leave it running and switch to your browser for the next substep.
You should see output indicating the server is running at http://127.0.0.1:8000.
Seeing an error when running fastapi dev?
- Make sure your virtual environment is active ((.venv) in prompt). The fastapi command only exists inside the environment where you installed it.
- Check that you're in the bookmark-api folder where main.py lives. The command looks for that file in the current directory.
help me fix the fastapi dev command not working
Verify the server and explore the auto-docs
Your server is running. Before you visit it, what do you think you'll see when you open the URL in your browser?
- Open your web browser and visit http://127.0.0.1:8000.
You should see a JSON response:
{"message": "Bookmark Manager API", "docs": "/docs"}
That JSON is the dictionary your root() function returns, automatically serialized by FastAPI.
- Now visit http://127.0.0.1:8000/docs in your browser.
You should see an interactive documentation page called Swagger UI. It lists your GET / endpoint and lets you test it directly from the browser.
What is Swagger UI?
FastAPI auto-generates this interactive documentation page from your code. Every endpoint you add automatically appears here with a "Try it out" button.
You'll use this page throughout the project to test your API instead of writing curl commands or installing separate tools.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and leave the server running in your terminal.
ⓧ I'd like to double check the full code
Here's what your main.py should look like:
from fastapi import FastAPI
app = FastAPI(title="Bookmark Manager API")
@app.get("/")
def root():
return {"message": "Bookmark Manager API", "docs": "/docs"}
Don't see the docs page?
- Make sure your server is still running in the terminal. If you closed it, run fastapi dev main.py again.
- Check the URL carefully. It must be http://127.0.0.1:8000/docs with no trailing spaces or typos.
help me get the FastAPI docs page working
Your development environment is ready and your first endpoint is live. Next up, you'll build the endpoints that actually create and list bookmarks.
Create and List Bookmarks
Your server is running and responding to requests. Now it is time to make it do something useful.
The core of any REST API is accepting data and giving it back. In this step, you will build two endpoints: one that lets you save a bookmark, and one that lists everything you have saved.
In this step, get ready to:
- Define data models that describe the shape of a bookmark.
- Build a POST endpoint that creates and stores a new bookmark.
- Build a GET endpoint that returns all stored bookmarks.
Define the data models
Before your API can accept or return bookmarks, it needs to know what a bookmark looks like. Pydantic models define the exact shape of data your API expects to receive and send back.
- In main.py, find the import at the very top of the file:
from fastapi import FastAPI
- Replace that single line with these three imports:
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
- Now add the data models and storage below the app = FastAPI(title="Bookmark Manager API") line by copying this code:
# Defines what fields the API expects when creating a bookmark
class BookmarkCreate(BaseModel):
url: str
title: str
# Defines what the API sends back (includes generated fields)
class Bookmark(BaseModel):
id: int
url: str
title: str
created_at: str
# In-memory storage and auto-increment counter
bookmarks: dict[int, dict] = {}
next_id: int = 1
What does this code do?
BookmarkCreate is the model for incoming requests. It tells FastAPI exactly which fields a client must send (url and title). If a request is missing either field, FastAPI automatically rejects it with a helpful error.
Bookmark is the response model. It adds id and created_at because those values are generated by your server, not sent by the client.
The bookmarks dictionary stores everything in memory for now. The next_id counter gives each bookmark a unique number.
- Save main.py.
- Check your terminal where the server is running. You should see a line indicating it reloaded successfully with no errors.
Seeing an error in the terminal?
Make sure the new classes are placed below the app = FastAPI(...) line, not above it.
Check that from pydantic import BaseModel is on its own line and spelled exactly. A missing import causes a NameError on reload.
help me fix a reload error in my FastAPI app after adding Pydantic models
Build the POST endpoint
Your models define the shape of the data. Now you need an endpoint that actually receives a bookmark from a client, generates an ID and timestamp, stores it, and sends the full bookmark back.
- Add the POST endpoint below the existing root() function by copying this code:
@app.post("/bookmarks", status_code=201)
def create_bookmark(bookmark: BookmarkCreate) -> Bookmark:
global next_id
# Build the full bookmark with auto-generated fields
new_bookmark = {
"id": next_id,
"url": bookmark.url,
"title": bookmark.title,
"created_at": datetime.now().isoformat(),
}
# Store it and increment the ID counter
bookmarks[next_id] = new_bookmark
next_id += 1
return new_bookmark
What does this code do?
The @app.post("/bookmarks", status_code=201) decorator tells FastAPI this function handles POST requests to /bookmarks. The 201 status code means "Created" and signals to the client that a new resource was successfully made.
FastAPI automatically parses the request body into a BookmarkCreate object because of the type hint. The function then builds a full bookmark dictionary, stores it, and returns it.
- Save main.py.
- Switch to your browser and visit http://127.0.0.1:8000/docs.
- Click the POST /bookmarks row to expand it.
- Click Try it out.
- Replace the example body with this JSON:
{
"url": "https://fastapi.tiangolo.com",
"title": "FastAPI Documentation"
}
- Click Execute.
You should see a 201 response with your bookmark returned, now including an id of 1 and a created_at timestamp.
Seeing a 422 or 500 error?
A 422 means your JSON body is malformed or missing required fields. Make sure both url and title are present and wrapped in double quotes.
A 500 means something crashed in your function. Check the terminal for the full traceback. Common causes: a typo in bookmark.url or a missing global next_id line.
help me debug my POST /bookmarks endpoint
Build the GET endpoint
You can create bookmarks, but you have no way to see what you have saved. The GET endpoint returns your full collection as a list.
- Add the GET endpoint below the create_bookmark function by copying this code:
@app.get("/bookmarks")
def list_bookmarks() -> list[Bookmark]:
# Convert the dict values to a list for the response
return list(bookmarks.values())
What does this code do?
The -> list[Bookmark] return type tells FastAPI to format the response as a JSON array of bookmark objects. FastAPI uses this type hint to validate and serialize your data automatically.
bookmarks.values() returns all stored bookmark dictionaries. Wrapping it in list() converts it from a dict view into a plain list that FastAPI can serialize to JSON.
- Save main.py.
Before you test, think about what you expect to see. You created one bookmark with the POST request earlier. What do you think GET /bookmarks will return?
- In the /docs page, click the GET /bookmarks row to expand it.
- Click Try it out, then click Execute.
You should see a JSON array containing the bookmark you created earlier, complete with its id and created_at fields.
- Create 1-2 more bookmarks using the POST endpoint with different URLs and titles.
- Run GET /bookmarks again and confirm all your bookmarks appear in the list.
GET returns an empty list?
If the server reloaded between your POST and GET requests, your bookmarks are gone. Data only lives in memory right now, so a reload wipes the slate clean. Create a new bookmark and immediately list them without restarting.
If you see an empty list even without a reload, confirm your create_bookmark function has the line bookmarks[next_id] = new_bookmark before the next_id += 1 line.
help me figure out why my GET /bookmarks returns an empty list
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py and that your server is still running in the terminal.
ⓧ I'd like to double check the full code
Your main.py should now contain these sections in order. Scroll up and confirm each is present:
- Three import lines at the top (fastapi, pydantic, datetime).
- The app = FastAPI(...) line.
- BookmarkCreate model with url and title fields.
- Bookmark model with id, url, title, and created_at fields.
- The bookmarks dictionary and next_id counter.
- The root() function (unchanged from Step 1).
- The create_bookmark function with the @app.post decorator.
- The list_bookmarks function with the @app.get decorator.
You have a working API that can save and retrieve bookmarks. There is one problem though: if you stop the server right now, everything disappears. Next up, you will fix that by persisting your bookmarks to a file.
Persist Bookmarks to a File
Your API can create and list bookmarks. But there is a serious problem lurking underneath.
Every time you restart the server, all your bookmarks vanish. They only live in a Python dictionary that gets destroyed when the process stops. In this step, you'll fix that by saving bookmarks to a JSON file so they survive restarts.
In this step, get ready to:
- Experience data loss by restarting the server.
- Add file persistence with load and save functions.
- Verify bookmarks survive a server restart.
Experience the problem
Right now, your bookmarks live only in a Python dictionary in memory. What happens to that dictionary when the server process stops?
- Make sure you have at least one bookmark created from the previous step. If not, visit http://127.0.0.1:8000/docs and use the POST endpoint to create one now.
Before you restart, think about what you expect to happen to your bookmarks. Will they still be there?
- Stop the server by pressing Ctrl+C in your terminal.
- Restart the server by running:
fastapi dev main.py
- In the /docs page, expand the GET /bookmarks endpoint, click Try it out, then click Execute.
You'll see an empty list. All your bookmarks are gone. The dictionary was destroyed when the server process stopped, and a fresh empty dictionary was created on restart.
Why did this happen?
Python variables only exist while the program is running. When you press Ctrl+C, the entire process ends and all its memory is released. Restarting creates a brand new process with a fresh, empty bookmarks dictionary.
Add file persistence functions
The fix is to write bookmarks to a file on disk whenever they change, and read that file back when the server starts. This way, your data lives outside the Python process.
- In main.py, find the imports at the top of your file:
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
- Add import json and from pathlib import Path below the existing imports. Then add a DATA_FILE constant below the app line, so the top of your file looks like this:
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
import json
from pathlib import Path
app = FastAPI(title="Bookmark Manager API")
DATA_FILE = Path("bookmarks.json")
Now you'll add the two functions that handle reading and writing the file.
- In main.py, find the line next_id: int = 1. Add the following code below it:
def load_bookmarks():
global bookmarks, next_id
if DATA_FILE.exists():
data = json.loads(DATA_FILE.read_text())
bookmarks = {item["id"]: item for item in data}
if bookmarks:
next_id = max(bookmarks.keys()) + 1
def save_bookmarks():
DATA_FILE.write_text(json.dumps(list(bookmarks.values()), indent=2))
load_bookmarks()
What does this code do?
These two functions handle all the file I/O for your bookmarks:
- load_bookmarks() checks if bookmarks.json exists. If it does, it reads the file, rebuilds the bookmarks dictionary, and sets next_id to one higher than the largest existing ID.
- save_bookmarks() writes the entire bookmarks dictionary to bookmarks.json as formatted JSON.
- The bare load_bookmarks() call at the bottom runs once when the server starts, restoring any previously saved data.
- Save main.py.
Check your terminal. The dev server auto-reloads when you save. You should see a message about detecting changes followed by the server starting up again without errors.
Seeing a syntax error in the terminal?
- Make sure load_bookmarks() is called at the module level (no indentation), not inside a function.
- Check that the def load_bookmarks(): and def save_bookmarks(): lines are placed after next_id: int = 1 and before the route definitions.
- Confirm the import json and from pathlib import Path lines are present at the top of the file.
Help me fix a syntax error in my FastAPI bookmark persistence code
The server restarted successfully, but you still need to hook save_bookmarks() into the endpoint that creates bookmarks.
- In main.py, find these lines inside the create_bookmark function:
bookmarks[next_id] = new_bookmark
next_id += 1
return new_bookmark
- Add a call to save_bookmarks() between next_id += 1 and return new_bookmark, so the end of the function looks like this:
bookmarks[next_id] = new_bookmark
next_id += 1
save_bookmarks()
return new_bookmark
- Save main.py.
Verify persistence works
Time to prove that your bookmarks can survive a restart.
- In the /docs page, use the POST /bookmarks endpoint to create a bookmark. Use any URL and title you like.
- Confirm that bookmarks.json appeared in your project folder by running:
ls
You should see bookmarks.json listed alongside main.py and the .venv folder.
Now for the real test. Before you restart, predict: will your bookmarks still be there this time?
- Stop the server with Ctrl+C.
- Restart the server by running:
fastapi dev main.py
- In the /docs page, expand GET /bookmarks, click Try it out, then click Execute.
Your bookmarks are still there. The load_bookmarks() call read bookmarks.json on startup and restored everything.
Still seeing an empty list after restart?
- Open bookmarks.json in your editor. If it's empty or missing, save_bookmarks() is not being called. Check that it appears inside create_bookmark after next_id += 1.
- If bookmarks.json has data but the API returns empty, check that load_bookmarks() is called at the module level (not inside another function). It must run when the file is imported.
- Make sure you saved main.py after adding all the changes.
Help me debug why my bookmarks aren't persisting after a server restart
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and your server is running without errors.
ⓧ I'd like to double check the full code
Scroll through your main.py and confirm these sections are present in order:
- Imports: FastAPI, BaseModel, datetime, json, Path all at the top.
- DATA_FILE = Path("bookmarks.json") below the app line.
- BookmarkCreate and Bookmark models unchanged.
- bookmarks dict and next_id counter.
- load_bookmarks() function, then save_bookmarks() function, then a bare load_bookmarks() call.
- The root, create_bookmark (now with save_bookmarks() before the return), and list_bookmarks endpoints.
Your bookmarks now survive restarts. Next up, you'll add the ability to update and delete bookmarks, with proper error handling when a bookmark doesn't exist.
Update and Delete with Error Handling
Your API can create and list bookmarks, but what happens if someone requests a bookmark that doesn't exist? Right now, that request crashes your server with an unhelpful 500 Internal Server Error.
In this step, you'll see the crash firsthand, then fix it with proper error handling that returns clean JSON error messages. You'll also add PUT and DELETE endpoints to complete the full CRUD cycle (Create, Read, Update, Delete).
In this step, get ready to:
- See your server crash when requesting a non-existent bookmark.
- Add proper error handling with meaningful HTTP status codes.
- Add PUT and DELETE endpoints to complete CRUD operations.
See the problem: request a non-existent bookmark
Before adding error handling, you need to see what happens without it. You'll add a basic get-by-ID endpoint that directly accesses the bookmarks dictionary with no safety check.
- In main.py, add this endpoint at the bottom of the file (below the list_bookmarks function):
@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int) -> Bookmark:
return bookmarks[bookmark_id]
What does this code do?
This endpoint takes a bookmark ID from the URL path and looks it up directly in the bookmarks dictionary. There's no check for whether that ID actually exists.
- Save main.py.
Before you test this, think: what do you expect to happen if you request a bookmark ID that doesn't exist in your dictionary?
- Switch to your browser and open http://127.0.0.1:8000/docs.
- Find the new GET /bookmarks/{bookmark_id} endpoint, click Try it out.
- Enter 999 as the bookmark_id and click Execute.
You'll see a 500 Internal Server Error response. The server crashed because Python raised a KeyError when trying to access a key that doesn't exist in the dictionary.
Why is a 500 error bad here?
A 500 error means "something went wrong on the server" and gives the client zero information about what they did wrong. In this case, the client simply asked for a bookmark that doesn't exist.
The correct response is a 404 ("not found") with a helpful message explaining that the requested bookmark ID doesn't exist. That tells the client exactly what went wrong so they can fix their request.
Fix with proper error handling
FastAPI provides HTTPException for returning clean error responses. You raise it like a normal Python exception, and FastAPI converts it into a JSON response with the status code and message you specify.
- In main.py, find the import line at the top of the file:
from fastapi import FastAPI
- Replace it with this line to add HTTPException and status:
from fastapi import FastAPI, HTTPException, status
What are HTTPException and status?
HTTPException is a Python exception you raise (not return) to immediately stop the request and send an error response to the client. The status module provides named constants like HTTP_404_NOT_FOUND so you don't have to memorize numeric codes.
- While you have the status module imported, find the create_bookmark decorator line:
@app.post("/bookmarks", status_code=201)
- Replace 201 with the named constant so the line reads:
@app.post("/bookmarks", status_code=status.HTTP_201_CREATED)
- Now find the get_bookmark function you just added and replace the entire function with this version:
@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int) -> Bookmark:
# Check if the bookmark exists before accessing it
if bookmark_id not in bookmarks:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bookmark with id {bookmark_id} not found",
)
return bookmarks[bookmark_id]
What changed?
Instead of blindly accessing the dictionary, the function now checks if the ID exists first. If it doesn't, it raises an HTTPException with a 404 status code and a helpful detail message. FastAPI catches this exception and returns a clean JSON response to the client.
- Save main.py.
- Return to the /docs page in your browser and try GET /bookmarks/999 again.
This time you'll see a clean 404 response with the body {"detail": "Bookmark with id 999 not found"}.
Still seeing 500 Internal Server Error?
- Make sure you saved main.py after making the changes. The dev server auto-reloads on save.
- Check that your import line reads from fastapi import FastAPI, HTTPException, status with all three items.
- Verify the if bookmark_id not in bookmarks: check comes before the return statement.
Still stuck? Why am I still getting a 500 error after adding HTTPException?
Add PUT and DELETE endpoints
With error handling in place, you can now add the remaining two CRUD operations. Both will use the same pattern: check if the bookmark exists, raise a 404 if not, then perform the operation.
- In main.py, add the PUT endpoint below the get_bookmark function:
@app.put("/bookmarks/{bookmark_id}")
def update_bookmark(bookmark_id: int, bookmark: BookmarkCreate) -> Bookmark:
# Return 404 if bookmark doesn't exist
if bookmark_id not in bookmarks:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bookmark with id {bookmark_id} not found",
)
# Update the bookmark's fields
bookmarks[bookmark_id].update(
{
"url": bookmark.url,
"title": bookmark.title,
}
)
save_bookmarks()
return bookmarks[bookmark_id]
What does the PUT endpoint do?
- It accepts the same BookmarkCreate body as the POST endpoint (a URL and title).
- It checks if the bookmark exists, raising a 404 if not.
- It updates only the url and title fields while preserving the original id and created_at.
- It calls save_bookmarks() so the update persists to the JSON file.
- Save main.py and test the PUT endpoint in /docs.
- First create a bookmark with POST, note its ID, then use PUT to change its title.
- Verify the response shows the updated title with the same ID and timestamp.
Getting a 422 Validation Error on PUT?
Make sure you're providing both url and title in the request body. The BookmarkCreate model requires both fields.
Need help? Why is my PUT request returning a 422 error?
- Now add the DELETE endpoint below update_bookmark:
@app.delete("/bookmarks/{bookmark_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bookmark(bookmark_id: int):
# Return 404 if bookmark doesn't exist
if bookmark_id not in bookmarks:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Bookmark with id {bookmark_id} not found",
)
del bookmarks[bookmark_id]
save_bookmarks()
Why does DELETE use status 204?
A 204 (No Content) response means "the action succeeded but there's nothing to send back." Since a delete removes the resource, there's no body to return. This is the standard convention for successful DELETE operations in REST APIs.
- Save main.py.
Time to test the full CRUD cycle. Before you run through it, predict: what status code do you think each operation will return?
- In the /docs page, run through this sequence:
- POST a new bookmark. Confirm you get a 201 response with the bookmark data.
- GET that bookmark by its ID. Confirm you get a 200 response.
- PUT to update its title. Confirm you get a 200 with the new title.
- DELETE it by ID. Confirm you get a 204 with no response body.
- GET the same ID again. Confirm you now get a 404 with a helpful error message.
That final 404 confirms the full cycle works: you created a bookmark, read it, updated it, deleted it, and verified it's gone.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and all five CRUD endpoints are working in your /docs page.
ⓧ I'd like to double check the full code
Your main.py file is now long, so here's what you should see in order from top to bottom:
- Imports: FastAPI, HTTPException, status from fastapi. BaseModel from pydantic. datetime, json, Path.
- App creation and DATA_FILE constant.
- BookmarkCreate and Bookmark Pydantic models.
- bookmarks dict and next_id counter.
- load_bookmarks() and save_bookmarks() functions, with load_bookmarks() called at module level.
- GET / root endpoint.
- POST /bookmarks with status_code=status.HTTP_201_CREATED.
- GET /bookmarks (list all).
- GET /bookmarks/{bookmark_id} with 404 handling.
- PUT /bookmarks/{bookmark_id} with 404 handling and save_bookmarks().
- DELETE /bookmarks/{bookmark_id} with status_code=status.HTTP_204_NO_CONTENT, 404 handling, and save_bookmarks().
You now have a complete REST API with proper error handling. Next up, you'll add tags and filtering so users can organize and find their bookmarks by topic.
Filter Bookmarks by Tag
Your API now handles the full lifecycle of a bookmark: creating, reading, updating, and deleting. But a flat list of bookmarks becomes useless as your collection grows.
In this step, you'll add a tags field to your bookmarks and teach your GET endpoint to filter by tag using a query parameter. This is how real REST APIs let consumers find exactly what they need without downloading everything.
In this step, get ready to:
- Add a tags field to your data models so bookmarks can be categorized.
- Implement query parameter filtering so the API returns only matching bookmarks.
- Test the filtering through the interactive docs page.
Add tags to the data model
Every bookmark can belong to multiple categories. A tags field gives each bookmark a list of labels like "python" or "tutorial".
- In main.py, find the BookmarkCreate class:
class BookmarkCreate(BaseModel):
url: str
title: str
- Add a tags field below title, so the class looks like this:
class BookmarkCreate(BaseModel):
url: str
title: str
tags: list[str] = []
- Find the Bookmark class directly below it:
class Bookmark(BaseModel):
id: int
url: str
title: str
created_at: str
- Add the same tags field below title, so it looks like this:
class Bookmark(BaseModel):
id: int
url: str
title: str
tags: list[str] = []
created_at: str
What does tags: list[str] = [] mean?
This is a Pydantic field with a default value. The list[str] type means the field expects a list of strings. The = [] default means tags are optional. If someone creates a bookmark without specifying tags, it defaults to an empty list.
Now you need to actually store the tags when a bookmark is created. The create_bookmark function builds a dictionary but doesn't include tags yet.
- Scroll down to the create_bookmark function and find the new_bookmark dictionary:
new_bookmark = {
"id": next_id,
"url": bookmark.url,
"title": bookmark.title,
"created_at": datetime.now().isoformat(),
}
- Add "tags": bookmark.tags, below the "title" line, so the dictionary looks like this:
new_bookmark = {
"id": next_id,
"url": bookmark.url,
"title": bookmark.title,
"tags": bookmark.tags,
"created_at": datetime.now().isoformat(),
}
- Save main.py.
- Switch to your browser and open http://127.0.0.1:8000/docs.
- Expand the POST /bookmarks section, click Try it out, and send this request body:
{
"url": "https://docs.python.org",
"title": "Python Docs",
"tags": ["python", "reference"]
}
- Click Execute.
You should see a 201 response with your bookmark, including the tags array in the JSON output.
Not seeing the tags field in the response?
- Make sure you added tags: list[str] = [] to BOTH BookmarkCreate and Bookmark models.
- Check that "tags": bookmark.tags, is inside the new_bookmark dictionary in create_bookmark.
- Confirm the server reloaded after you saved. You should see a reload message in your terminal.
help me debug why tags aren't showing in my bookmark response
Now update the update_bookmark function so tags can be changed after creation.
- Find the update_bookmark function and locate the .update() call:
bookmarks[bookmark_id].update(
{
"url": bookmark.url,
"title": bookmark.title,
}
)
- Add "tags": bookmark.tags, below the "title" line, so it looks like this:
bookmarks[bookmark_id].update(
{
"url": bookmark.url,
"title": bookmark.title,
"tags": bookmark.tags,
}
)
- Save main.py.
- In the /docs page, use the PUT /bookmarks/{bookmark_id} endpoint to update your bookmark's tags. Use the ID from the bookmark you just created.
The response should show the updated tags.
Add query parameter filtering
Right now, GET /bookmarks always returns every bookmark. A query parameter lets the caller append ?tag=python to the URL to filter results without needing a whole new endpoint.
- In main.py, find the list_bookmarks function:
@app.get("/bookmarks")
def list_bookmarks() -> list[Bookmark]:
return list(bookmarks.values())
- Replace the entire function with this updated version that accepts an optional tag parameter:
@app.get("/bookmarks")
def list_bookmarks(tag: str | None = None) -> list[Bookmark]:
# Start with all bookmarks
all_bookmarks = list(bookmarks.values())
# If a tag filter was provided, keep only matching bookmarks
if tag:
all_bookmarks = [b for b in all_bookmarks if tag in b["tags"]]
return all_bookmarks
What does this code do?
- tag: str | None = None declares an optional query parameter. When someone calls /bookmarks?tag=python, FastAPI automatically reads the value from the URL and passes it to your function.
- When tag is None (no filter requested), the function returns all bookmarks. When a tag is provided, the list comprehension keeps only bookmarks whose tags list contains that value.
- Save main.py.
Seeing a KeyError on tags?
If you have bookmarks left over from earlier steps, they were saved without a tags field. Delete bookmarks.json from your project folder. The server will reload with no bookmarks, and you can create fresh ones with tags.
help me fix a KeyError when filtering bookmarks by tag
Test filtering
Time to see the filter in action. You'll create bookmarks with different tags, then prove that the query parameter returns only matching results.
- If you have old bookmarks from earlier steps, delete bookmarks.json from your project folder so you start with a clean slate.
- In the /docs page, use POST /bookmarks to create a bookmark with tags ["python", "tutorial"].
- Create a second bookmark with tags ["javascript", "tutorial"].
- Create a third bookmark with tags ["python", "reference"].
Before you test the filter, predict: if you call /bookmarks?tag=python, how many bookmarks do you expect to see?
- Expand the GET /bookmarks section and click Try it out.
- Type python in the tag parameter field, then click Execute.
You should see exactly 2 bookmarks in the response: only the ones tagged "python". The JavaScript bookmark is filtered out.
- Clear the tag field and click Execute again.
All 3 bookmarks appear. When no tag is provided, the filter is skipped and everything is returned.
Still seeing all bookmarks when filtering?
- Make sure you replaced the entire list_bookmarks function, not just the signature. The if tag: filtering logic must be inside the function body.
- Check that you typed the tag value in the parameter field, not in the URL bar directly.
- Confirm the server reloaded by checking the terminal for a reload message after your last save.
help me debug why my tag filter isn't working in FastAPI
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and your server is still running.
ⓧ I'd like to double check the full code
Your main.py is now longer than a quick glance, so scroll through and confirm these sections are present in order:
- Imports (FastAPI, HTTPException, status, BaseModel, datetime, json, Path).
- App and DATA_FILE constant.
- BookmarkCreate model with url, title, and tags: list[str] = [].
- Bookmark model with id, url, title, tags: list[str] = [], and created_at.
- bookmarks dict and next_id counter.
- load_bookmarks() and save_bookmarks() functions, with load_bookmarks() called at module level.
- Root endpoint (GET /).
- create_bookmark with "tags": bookmark.tags in the dictionary.
- list_bookmarks with tag: str | None = None parameter and filtering logic.
- get_bookmark, update_bookmark (with "tags": bookmark.tags in the update dict), and delete_bookmark.
You've just turned a basic list endpoint into a searchable collection. Your API consumers can now ask for exactly the bookmarks they want without downloading everything and filtering on their end. Next up, you'll take on the Secret Mission and add full-text search plus a stats endpoint.
Secret mission
Add Search and Stats
Your API can store and filter bookmarks, but it can't search across them or summarize your collection. Real APIs need discovery features. In this mission, you'll add full-text search and a stats endpoint from scratch.
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:
- The bookmark-api project folder on your Desktop.
- A Python virtual environment in .venv/ (local to your project folder).
- A bookmarks.json data file storing your bookmarks.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your API.
- Your development server continues running at http://127.0.0.1:8000.
- Your bookmarks persist in bookmarks.json and survive server restarts.
- Everything is local. There are no cloud resources or running costs.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your project files stay on disk and are ready to resume anytime.
- In your terminal, press Ctrl+C to stop the development server.
- To resume later, open your terminal in the bookmark-api folder, activate the virtual environment with source .venv/bin/activate, and run fastapi dev main.py.
Delete - I don't want to use this again
Remove all project files and start fresh. Since everything is local, deleting the project folder removes everything at once.
- In your terminal, press Ctrl+C to stop the development server if it is still running.
- Deactivate your virtual environment by running:
deactivate
- Navigate to your Desktop and delete the entire project folder by running:
cd ~/Desktop
rm -rf bookmark-api
On Windows, use this instead:
cd ~\Desktop
rmdir /s /q bookmark-api
- This removes main.py, bookmarks.json, the .venv/ virtual environment, and all other project files.
Nice Work!
Nice Work!
You just built a fully functional REST API from scratch using Python and FastAPI. You can create, read, update, and delete bookmarks, filter them by tag, and your data persists across server restarts.
You've learned how to:
- Build a complete REST API with full CRUD operations using FastAPI, including proper HTTP status codes like 201 Created, 204 No Content, and 404 Not Found.
- Validate incoming data automatically with Pydantic models and return meaningful error responses using HTTPException when something goes wrong.
- Persist data to a JSON file so state survives server restarts, and add query parameter filtering so API consumers can find bookmarks by tag.
- Secret Mission: Added full-text search across bookmark titles and URLs, plus a stats endpoint for collection insights.
Ready to quiz yourself?