Build a Personal Bookmarks API

Build a REST API with full CRUD operations using FastAPI and Pydantic.

Introduction

30 Second Summary

Every app you use daily talks to a backend API behind the scenes. When you save a song to a playlist, add an item to your cart, or bookmark a page, that action travels to a server that stores it and sends back a response.

In this project, you will build a REST API from scratch using Python and FastAPI that stores, retrieves, updates, and deletes bookmarks.

What You'll Build

You'll have a live API running on your machine where you can add bookmarks, fetch them back, update their details, and delete them through an interactive browser interface that sends real HTTP requests and shows real JSON responses.

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

  • A running REST API with full CRUD endpoints (Create, Read, Update, Delete) that you can demo in your browser.
  • Automatic data validation powered by Pydantic that rejects malformed requests with clear error messages.
  • Interactive API documentation at /docs where you can test every endpoint without installing any extra tools.
  • Secret Mission: Add tag-based filtering and search to query your bookmarks collection.

Are there any prerequisites?

You should be comfortable with basic Python concepts like functions, dictionaries, and type hints. No prior web development or API experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Naming your goal now turns the next 45 minutes from "following instructions" into building something purposeful.

Set Up Your Project

To build a REST API, you need a modern Python environment and the FastAPI framework installed. Your system currently has Python 3.9, which is too old for the latest FastAPI.

Instead of upgrading Python manually, you will use uv, a fast Python package and project manager that downloads the correct Python version for you automatically. By the end of this step, you will have a running FastAPI server you can see in your browser.

In this step, get ready to:
  • Install uv, the Python project manager.
  • Create a new project with Python 3.13 and add FastAPI.
  • Run your first API endpoint and see it in the browser.
Install uv

uv is the package and project manager recommended by FastAPI's official documentation. It handles Python version management, dependency installation, and virtual environments in a single tool.

  • Open a terminal in VS Code by pressing Ctrl+` (macOS and Windows).
  • Install uv by running this command:

macOS

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

What does this command do?

It downloads and runs the official uv installer from Astral (the company behind uv). The installer places the uv binary on your system and adds it to your PATH so you can use it from any terminal.

  • Close your terminal and open a new one (press Ctrl+` twice) so the PATH changes take effect.
  • Verify uv is installed by running:
uv --version

✔️ I see a version number

uv is ready to go. Move on to the next substep.

ⓧ Command not found

Your terminal does not have uv on its PATH yet. Try these fixes:

  • Make sure you fully closed and reopened the terminal after installing.
  • On macOS, try opening a brand new Terminal window outside VS Code, run uv --version there, then relaunch VS Code.
  • On Windows, close and reopen VS Code entirely so the new PATH is picked up.

If it still does not work, help me troubleshoot uv installation.

Create the project and install FastAPI

uv can create a project directory, download the right Python version, and set up an isolated environment in a single command. No need to install Python 3.13 separately.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project with Python 3.13 by running this command:
uv init bookmarks-api --python 3.13

You should see output like Initialized project `bookmarks-api` confirming the project was created.

What did uv just do?

uv created a bookmarks-api folder with a pyproject.toml (the project manifest), a main.py starter file, and a .python-version file pinning Python 3.13.

If Python 3.13 is not already on your machine, uv downloads and manages it for you behind the scenes.

  • Move into the project folder and install FastAPI with all standard extras by running these commands:
cd bookmarks-api
uv add "fastapi[standard]"

You will see uv create a virtual environment, resolve packages, and install FastAPI along with Uvicorn (the server that runs your API).

What are standard extras?

The [standard] extra installs everything you need for local development, including the Uvicorn web server, the fastapi CLI tool, and auto-reload support. Without it you would need to install these pieces separately.

Seeing a resolution error?

If uv add fails with a resolution error, make sure you typed the quotes around "fastapi[standard]" exactly as shown. Some shells interpret square brackets as special characters without the quotes.

Still stuck? Help me fix a uv add error.

Write your first endpoint and run the server

FastAPI apps are built from Python functions decorated with HTTP method annotations. You will replace the generated main.py with a minimal app that responds to GET requests.

  • Open the bookmarks-api folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the bookmarks-api folder, and click Open.
  • In the Explorer sidebar, click main.py to open it.
  • Select all the existing content and replace it with the following code:
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {"status": "Bookmarks API is running"}

What does this code do?

  • from fastapi import FastAPI imports the framework.
  • app = FastAPI() creates your application instance. All endpoints attach to this object.
  • @app.get("/") registers the function below it as the handler for GET requests to the root path.
  • The function returns a Python dictionary, which FastAPI automatically converts to a JSON response.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open a new terminal in VS Code (Ctrl+`) and start the FastAPI development server by running:
uv run fastapi dev main.py

What does this command do?

uv run executes a command inside the project's virtual environment so all installed packages are available. fastapi dev starts Uvicorn in development mode with auto-reload enabled, meaning the server restarts automatically when you save changes.

Before you check the result, what do you expect to see when you visit the root URL in your browser?

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

You should see the JSON response {"status":"Bookmarks API is running"} displayed in the browser.

  • Now visit http://127.0.0.1:8000/docs in your browser.

You should see the auto-generated Swagger UI with your single GET / endpoint listed. This interactive documentation is built automatically from your code by FastAPI.

Don't see the JSON or Swagger UI?

  • Check the terminal for errors. If you see ModuleNotFoundError, make sure you ran uv add "fastapi[standard]" successfully in the previous substep.
  • Make sure the terminal shows the server running with no tracebacks. The output should mention Uvicorn running on http://127.0.0.1:8000.
  • If the browser shows "This site can't be reached", confirm the terminal is still running the server (do not press Ctrl+C).

Still stuck? Help me get my FastAPI server running.

✔️ Awesome, I've got everything!

Great. Double check that you have saved main.py and that the server is still running in your terminal.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {"status": "Bookmarks API is running"}

Your development environment is ready and you have a live API responding to requests. Next up, you will build real CRUD endpoints that create, read, and return bookmarks.

Build Your First CRUD Endpoints

In Step 1, you got your API server running and saw a single JSON response in the browser. Now it's time to make that API actually do something.

You'll build endpoints that let you create, store, and retrieve bookmarks. By the end of this step, you'll have a working baby version of the API with data flowing through it.

In this step, get ready to:
  • Add an in-memory data store and a POST endpoint for creating bookmarks.
  • Add GET endpoints for retrieving bookmarks.
  • Test your API in the Swagger UI and discover it accepts garbage data.
Add the data store and create endpoint

Your API needs somewhere to put bookmarks when a client sends them. You'll use a Python dictionary as a simple in-memory store, then build a POST endpoint that accepts bookmark data and saves it.

  • In main.py, find this import line at the top:
from fastapi import FastAPI
  • Update it to also import Body, so it looks like this:
from fastapi import FastAPI, Body
  • Next, add these lines directly below app = FastAPI() and above the existing @app.get("/") decorator:
# In-memory data store
bookmarks: dict[int, dict] = {}
next_id: int = 1
  • Now add the POST endpoint below your root() function by copying this code:
@app.post("/bookmarks", status_code=201)
def create_bookmark(bookmark: dict = Body(...)):
    global next_id
    bookmark_data = {"id": next_id, **bookmark}
    bookmarks[next_id] = bookmark_data
    next_id += 1
    return bookmark_data

What does this code do?

bookmarks: dict[int, dict] = {} creates an empty dictionary that maps integer IDs to bookmark data. next_id tracks which ID to assign next.

The @app.post decorator tells FastAPI this function handles POST requests to /bookmarks. status_code=201 means the response signals "created" instead of a generic 200.

bookmark: dict = Body(...) tells FastAPI to read the raw JSON body as a plain Python dictionary. The ... means the body is required. No validation happens on its contents yet.

  • Save main.py.
  • Switch back to your browser and refresh http://127.0.0.1:8000/docs. You should see a new green POST /bookmarks section in the Swagger UI.

Don't see the POST endpoint?

  • Make sure your server is still running in the terminal. If you stopped it, restart it with uv run fastapi dev main.py.
  • Check that the @app.post decorator sits below the root() function, not inside it.
  • Confirm the import line reads from fastapi import FastAPI, Body with a capital B.

Still stuck? Why isn't my POST endpoint showing up in the FastAPI docs?

Add endpoints to retrieve bookmarks

Creating bookmarks is only useful if you can get them back out. You'll add two GET endpoints: one that lists all bookmarks, and one that fetches a single bookmark by its ID.

  • Add this endpoint below your create_bookmark() function in main.py:
@app.get("/bookmarks")
def list_bookmarks():
    return list(bookmarks.values())

What does this code do?

bookmarks.values() returns all the stored bookmark dictionaries. Wrapping it in list() converts it to a JSON-serializable list that FastAPI sends back as the response.

  • Save main.py and refresh /docs. You should see a blue GET /bookmarks section appear.
  • Now add this endpoint below list_bookmarks() to fetch a single bookmark by ID:
@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int):
    if bookmark_id not in bookmarks:
        return {"message": "Bookmark not found"}
    return bookmarks[bookmark_id]

What does this code do?

{bookmark_id} in the URL path is a path parameter. FastAPI automatically extracts it from the URL and converts it to an integer because of the bookmark_id: int type hint.

If the ID doesn't exist in the dictionary, the endpoint returns a simple message instead of crashing.

  • Save main.py and refresh /docs. You should now see three endpoints: POST, GET list, and GET single.

Seeing a server error in the terminal?

  • Check that get_bookmark is defined below list_bookmarks, not nested inside it. Each function starts at the same indentation level.
  • Make sure you have bookmark_id: int in the function parameter, matching the {bookmark_id} in the path string exactly.

Need help? Why am I getting a syntax or indentation error in my FastAPI endpoints?

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and your /docs page shows all three bookmark endpoints.

ⓧ I'd like to double check the full code

Here is what your complete main.py should look like at this point:

from fastapi import FastAPI, Body

app = FastAPI()

# In-memory data store
bookmarks: dict[int, dict] = {}
next_id: int = 1


@app.get("/")
def root():
    return {"status": "Bookmarks API is running"}


@app.post("/bookmarks", status_code=201)
def create_bookmark(bookmark: dict = Body(...)):
    global next_id
    bookmark_data = {"id": next_id, **bookmark}
    bookmarks[next_id] = bookmark_data
    next_id += 1
    return bookmark_data


@app.get("/bookmarks")
def list_bookmarks():
    return list(bookmarks.values())


@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int):
    if bookmark_id not in bookmarks:
        return {"message": "Bookmark not found"}
    return bookmarks[bookmark_id]
Test your endpoints and discover the gap

Your API has endpoints. Time to put them through their paces using the built-in Swagger UI at /docs.

  • In the Swagger UI, click the green POST /bookmarks row to expand it.
  • Click Try it out.
  • Replace the contents of the request body field with this JSON:
{
  "url": "https://fastapi.tiangolo.com",
  "title": "FastAPI Docs",
  "tags": ["python", "api"]
}
  • Click Execute.

You should see a 201 response with your bookmark data returned, now including an "id": 1 field.

  • Add one or two more bookmarks the same way (use different URLs and titles each time).
  • Expand the blue GET /bookmarks row, click Try it out, then click Execute.

You should see all your stored bookmarks returned as a JSON list.

Now for the interesting part. What do you think will happen if you send completely invalid data to the POST endpoint?

  • Go back to the POST /bookmarks section in Swagger UI.
  • Replace the request body with this nonsense:
{
  "nonsense": 123
}
  • Click Execute.

The API accepts it with a 201 status. No error, no complaint. Your garbage data is now stored as a bookmark.

  • Try one more: send an empty body {} to the POST endpoint.

That gets accepted too. Your API has no idea what a valid bookmark looks like.

Why does this matter?

In a real application, this garbage would corrupt your database. Imagine a frontend developer accidentally sends the wrong shape of data, or a malicious user sends random payloads. Your API happily stores all of it.

This is exactly the problem that data validation solves. In the next step, you'll add Pydantic models that reject invalid data automatically before your code even runs.

Your endpoints are working and data is flowing through the API. Next up, you'll add validation so your API can tell the difference between a real bookmark and garbage.

Add Data Validation with Pydantic

In the previous step, you saw your API happily accept garbage data. An empty body, missing fields, wrong types. All stored without complaint.

That would corrupt a real database in minutes. Pydantic models let FastAPI automatically reject invalid requests with clear error messages before your code even runs.

In this step, get ready to:
  • Define a Pydantic model that describes valid bookmark data.
  • See FastAPI return 422 errors when invalid data is submitted.
  • Add a PUT endpoint for partial updates using an optional-field model.
Define the BookmarkCreate model

A Pydantic BaseModel class defines the exact shape of valid incoming data. FastAPI uses it as a gatekeeper: if a request doesn't match the model, the server rejects it automatically.

  • In main.py, find the import line at the very top:
from fastapi import FastAPI, Body
  • Replace that entire line with these two import lines:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

Why these import changes?

You no longer need Body because the Pydantic model handles request body parsing automatically. HTTPException is imported for proper error handling you'll use shortly. BaseModel is the Pydantic class your models will inherit from.

  • Now add the BookmarkCreate model below the next_id: int = 1 line by pasting this code:
# Pydantic models
class BookmarkCreate(BaseModel):
    url: str
    title: str
    tags: list[str] = []

What does this model do?

  • url: str and title: str are required fields. Any request missing them gets rejected.
  • tags: list[str] = [] is optional with a default empty list. If the client doesn't send tags, the bookmark still works.
  • Next, find the create_bookmark function. It currently looks like this:
@app.post("/bookmarks", status_code=201)
def create_bookmark(bookmark: dict = Body(...)):
    global next_id
    bookmark_data = {"id": next_id, **bookmark}
    bookmarks[next_id] = bookmark_data
    next_id += 1
    return bookmark_data
  • Replace the entire function with this version that uses the Pydantic model:
@app.post("/bookmarks", status_code=201)
def create_bookmark(bookmark: BookmarkCreate):
    global next_id
    bookmark_data = {"id": next_id, **bookmark.model_dump()}
    bookmarks[next_id] = bookmark_data
    next_id += 1
    return bookmark_data

What changed in this function?

  • The parameter type changed from dict = Body(...) to BookmarkCreate. FastAPI now validates the body against your model automatically.
  • bookmark.model_dump() converts the validated Pydantic model back into a dictionary for storage.
  • Save main.py. The dev server auto-reloads your changes.

Server showing an import error?

Make sure you removed Body from the fastapi import line completely. The new import should be from fastapi import FastAPI, HTTPException. Also check that from pydantic import BaseModel is on its own separate line.

help me fix my FastAPI import error

Test that validation blocks garbage data

Your model is in place. Time to see it in action by sending the same garbage data your API used to accept without complaint.

  • Open http://127.0.0.1:8000/docs in your browser.
  • Expand the POST /bookmarks endpoint and click Try it out.

Before you hit Execute, predict what will happen if you send an empty body {}. Will the API accept it like before, or reject it?

  • Replace the request body with {} and click Execute.

You get a 422 Unprocessable Entity response. The body includes "msg": "Field required" for both url and title. FastAPI rejected the request before your function even ran.

  • Now try sending {"nonsense": 123} as the body.

Same result. The model only accepts url, title, and tags. Everything else is ignored and required fields must be present.

  • Send a valid bookmark to confirm it still works:
{
  "url": "https://fastapi.tiangolo.com",
  "title": "FastAPI Docs",
  "tags": ["python", "api"]
}

You get a 201 response with your bookmark stored correctly, including its assigned id.

What is a 422 response?

HTTP status 422 means the server understood the request format (valid JSON) but the content failed validation. It tells the client exactly what went wrong so they can fix their request.

Add a BookmarkUpdate model and PUT endpoint

Creating bookmarks is covered. Now you need a way to update them. A PUT endpoint lets clients modify an existing bookmark without replacing the whole thing.

The trick is that updates should be partial. If someone only sends a new title, the URL and tags should stay the same.

  • In main.py, add the BookmarkUpdate model directly below the BookmarkCreate class by pasting this code:
class BookmarkUpdate(BaseModel):
    url: str | None = None
    title: str | None = None
    tags: list[str] | None = None

Why are all fields optional here?

Every field defaults to None, meaning the client only sends the fields they want to change. If title is sent but url is not, only the title updates.

The str | None = None syntax is Python 3.10+ union type syntax. It means the field accepts either a string or None.

  • Now find the get_bookmark function. It currently returns a plain message when a bookmark isn't found:
@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int):
    if bookmark_id not in bookmarks:
        return {"message": "Bookmark not found"}
    return bookmarks[bookmark_id]
  • Replace it with this version that raises a proper HTTP error:
@app.get("/bookmarks/{bookmark_id}")
def get_bookmark(bookmark_id: int):
    if bookmark_id not in bookmarks:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    return bookmarks[bookmark_id]

What does HTTPException do?

Raising HTTPException immediately stops the request and returns the specified status code. A 404 tells the client the resource doesn't exist. Returning a plain dict with a message gave a misleading 200 OK status.

  • Save main.py and confirm the server reloads in your terminal.
  • Open http://127.0.0.1:8000/docs and test GET /bookmarks/999. You should see a 404 response with {"detail": "Bookmark not found"}.

Still seeing a 200 OK with a message?

Make sure you replaced the entire get_bookmark function, including the return {"message": "Bookmark not found"} line. The new version uses raise HTTPException(...) instead of return.

help me fix my 404 response in FastAPI

  • Now add the PUT endpoint at the very end of main.py, below the get_bookmark function:
@app.put("/bookmarks/{bookmark_id}")
def update_bookmark(bookmark_id: int, bookmark_update: BookmarkUpdate):
    if bookmark_id not in bookmarks:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    stored = bookmarks[bookmark_id]
    update_data = bookmark_update.model_dump(exclude_unset=True)
    stored.update(update_data)
    return stored

What does this endpoint do?

  • bookmark_update: BookmarkUpdate accepts the partial update body. Only fields the client sends will have values.
  • model_dump(exclude_unset=True) returns only the fields the client explicitly sent. Fields left as None by default are excluded.
  • stored.update(update_data) merges the new values into the existing bookmark, preserving any fields the client didn't touch.
  • Save main.py.

Before testing, predict: if you PUT {"title": "Updated Title"} to a bookmark, what should happen to the url and tags fields?

  • In the Swagger UI at /docs, use POST /bookmarks to create a bookmark with this body:
{
  "url": "https://fastapi.tiangolo.com",
  "title": "FastAPI Docs",
  "tags": ["python", "api"]
}
  • Now expand PUT /bookmarks/{bookmark_id}, click Try it out, enter 1 for the bookmark_id, and set this request body:
{
  "title": "Updated Title"
}
  • Click Execute.

The response shows your bookmark with the title changed to "Updated Title" while url and tags remain untouched. That's the partial update working.

  • Use GET /bookmarks/1 to confirm the update persisted.

PUT returning a 404?

The in-memory store resets every time the server restarts. If you see a 404 on /bookmarks/1, create a new bookmark first with POST, then try the PUT again with the ID from the response.

help me debug my PUT endpoint returning 404

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and that the server reloaded without errors in your terminal.

ⓧ I'd like to double check the full code

Your main.py should have these sections in order. Scroll up and confirm each one is present:

  • Import lines: from fastapi import FastAPI, HTTPException and from pydantic import BaseModel.
  • In-memory data store (bookmarks dict and next_id).
  • BookmarkCreate model with url, title, and tags fields.
  • BookmarkUpdate model with all optional fields.
  • GET / root endpoint.
  • POST /bookmarks using BookmarkCreate and bookmark.model_dump().
  • GET /bookmarks returning all bookmarks.
  • GET /bookmarks/{bookmark_id} with HTTPException for missing IDs.
  • PUT /bookmarks/{bookmark_id} using BookmarkUpdate and model_dump(exclude_unset=True).

Your API now validates every incoming request and handles partial updates cleanly. Next up, you'll add proper error handling for missing bookmarks and a DELETE endpoint to remove them.

Add Error Handling and DELETE

Your API now validates incoming data and rejects malformed requests. The next piece of a complete REST API is the ability to remove resources.

Your GET and PUT endpoints already handle missing bookmarks with proper 404 errors. In this step, you'll add the final CRUD operation (DELETE) using the same error handling pattern, then verify the full lifecycle works end-to-end.

In this step, get ready to:
  • Add a DELETE endpoint that removes bookmarks by ID.
  • Verify 404 error responses for non-existent bookmarks.
  • Confirm the full delete lifecycle works from creation to removal.
Add the DELETE endpoint

The DELETE endpoint follows the same pattern as your GET and PUT handlers. It checks whether the bookmark exists, raises a 404 if it doesn't, and removes it from the store if it does.

  • In main.py, scroll to the bottom of the file (after your update_bookmark function) and add the DELETE endpoint:
@app.delete("/bookmarks/{bookmark_id}")
def delete_bookmark(bookmark_id: int):
    # Return 404 if bookmark doesn't exist
    if bookmark_id not in bookmarks:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    # Remove the bookmark from the store
    del bookmarks[bookmark_id]
    return {"message": "Bookmark deleted"}

What does this code do?

  • @app.delete("/bookmarks/{bookmark_id}") registers this function to handle HTTP DELETE requests at that path.
  • The if bookmark_id not in bookmarks check prevents the learner from deleting something that doesn't exist. It raises the same 404 pattern your GET and PUT endpoints use.
  • del bookmarks[bookmark_id] removes the entry from the dictionary permanently.
  • The function returns a confirmation message so the client knows the deletion succeeded.
  • Save main.py.
  • Your server auto-reloads. Open http://127.0.0.1:8000/docs and confirm you see a red DELETE entry for /bookmarks/{bookmark_id} in the Swagger UI.

Don't see the DELETE endpoint?

  • Make sure you saved main.py. The dev server only reloads on save.
  • Check your terminal for a syntax error. A missing colon or mismatched indentation stops the reload.
  • Hard-refresh the browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear the cached docs page.

Ask for help: Why isn't my DELETE endpoint showing up in Swagger UI?

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and the DELETE endpoint appears in your Swagger UI.

ⓧ 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:

  • Imports (fastapi, pydantic).
  • App instance and in-memory data store (bookmarks dict, next_id).
  • Pydantic models (BookmarkCreate and BookmarkUpdate).
  • GET / root endpoint.
  • POST /bookmarks (creates with 201 status).
  • GET /bookmarks (lists all).
  • GET /bookmarks/{bookmark_id} (404 if missing).
  • PUT /bookmarks/{bookmark_id} (partial update, 404 if missing).
  • DELETE /bookmarks/{bookmark_id} (removes bookmark, 404 if missing).
Test the complete delete lifecycle

The real proof of your API is watching the full lifecycle: create a bookmark, verify it exists, delete it, then confirm it's gone.

  • In the Swagger UI at /docs, expand the POST /bookmarks endpoint and click Try it out.
  • Paste this JSON into the request body and click Execute:
{
  "url": "https://fastapi.tiangolo.com",
  "title": "FastAPI Docs",
  "tags": ["python", "api"]
}
  • You should see a 201 response with an id of 1. Note this ID for the next tests.

Before you try the next request, predict what you'll see: what happens if you GET a bookmark ID that doesn't exist, like 999?

  • Expand GET /bookmarks/{bookmark_id}, click Try it out, enter 999 for the bookmark_id, and click Execute.

You get a 404 status with {"detail": "Bookmark not found"}. That's your HTTPException doing its job.

  • Now expand DELETE /bookmarks/{bookmark_id}, click Try it out, enter 1 for the bookmark_id, and click Execute.
  • You should see a 200 response with {"message": "Bookmark deleted"}.

Now predict again: what happens when you GET the bookmark you just deleted?

  • Switch back to GET /bookmarks/{bookmark_id}, enter 1, and click Execute.

You get a 404 again. The bookmark is gone. Try DELETEing ID 1 a second time and you'll also see 404, because you can't delete what's already removed.

Why does the second DELETE also return 404?

Once a bookmark is removed from the dictionary, any future request for that ID (GET, PUT, or DELETE) hits the if bookmark_id not in bookmarks check and raises the same 404 error.

This consistent behaviour makes your API predictable. Clients always know what a 404 means regardless of which operation triggered it.

Your API now handles every CRUD operation: Create, Read, Update, and Delete. Each endpoint communicates clearly when something goes wrong. Next up, the Secret Mission challenges you to add filtering and search to make this API truly useful.

Secret mission

Add Tag Filtering and Search

Your bookmarks API can store and retrieve data, but a real API needs to let clients search and filter efficiently. In this secret mission, you'll add query parameter filtering so users can find bookmarks by tag, search by keyword, and get a summary of all tags in use.

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 bookmarks-api/ project folder on your Desktop (contains main.py, pyproject.toml, and the .venv virtual environment).
  • The uv package manager (installed to ~/.local/bin/).
  • The FastAPI dev server process (running in your terminal).

Keep everything running

No action needed. Choose this if you plan to keep building on your bookmarks API or want to revisit it soon.

  • Your dev server stays running as long as your terminal is open.
  • The uv tool manager stays available for future Python projects.
  • Your bookmarks-api/ folder remains on your Desktop.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your project files stay on disk for next time.

  • Switch to the terminal running the dev server.
  • Press Ctrl+C to stop the server.
  • When you return, cd into your bookmarks-api/ folder and run uv run fastapi dev main.py to start up again.

Delete - I don't want to use this again

Remove all project files and optionally uninstall uv from your system.

  • Stop the dev server by pressing Ctrl+C in its terminal.
  • Delete the project folder by running:
cd ~/Desktop
rm -rf bookmarks-api/

To also remove uv and its cached data from your system, run:

uv cache clean
rm -r "$(uv python dir)"
rm -r "$(uv tool dir)"
rm ~/.local/bin/uv ~/.local/bin/uvx

What do these commands do?

  • uv cache clean removes downloaded packages and cached data.
  • The two rm -r commands remove Python versions and tools that uv installed.
  • The final rm removes the uv and uvx binaries themselves.

On Windows, remove the binaries with:

rm $HOME\.local\bin\uv.exe
rm $HOME\.local\bin\uvx.exe
rm $HOME\.local\bin\uvw.exe

Nice Work!

Nice Work!

You just built a fully functional REST API from scratch using FastAPI and Python. You can create, retrieve, update, and delete bookmarks through real HTTP requests, with automatic data validation and interactive documentation you can demo in a browser.

Here's what you learned:

  • Built a complete REST API with full CRUD operations (Create, Read, Update, Delete) using FastAPI route decorators and an in-memory data store.
  • Used Pydantic models for automatic data validation that rejects malformed requests with clear 422 error responses before your code even runs.
  • Implemented proper error handling with HTTPException and explored your entire API through auto-generated interactive Swagger UI documentation at /docs.
  • Secret Mission: Added query parameter filtering and search to make your bookmarks API production-like, including a tag summary endpoint.

Ready to quiz yourself?