Build a production-style REST API for managing bookmarks — CRUD operations, validation, database persistence, and tests.

What You’ll Build

  GET    /bookmarks/       List all bookmarks
POST   /bookmarks/       Create a bookmark
GET    /bookmarks/{id}   Get one bookmark
PUT    /bookmarks/{id}   Update a bookmark
DELETE /bookmarks/{id}   Delete a bookmark
GET    /docs             Auto-generated Swagger UI
  

Setup

  mkdir bookmark-api && cd bookmark-api
python -m venv .venv && source .venv/bin/activate
pip install "fastapi[standard]" sqlalchemy uvicorn pytest httpx
  

Project Structure

  bookmark-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── database.py
│   ├── models.py
│   ├── schemas.py
│   └── routers/
│       └── bookmarks.py
└── tests/
    └── test_bookmarks.py
  

Database Model

  # app/models.py
from sqlalchemy import String, Text, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base

class Bookmark(Base):
    __tablename__ = "bookmarks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    url: Mapped[str] = mapped_column(String(500))
    description: Mapped[str] = mapped_column(Text, default="")
    created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
  

Pydantic Schemas

  # app/schemas.py
from pydantic import BaseModel, HttpUrl, ConfigDict
from datetime import datetime

class BookmarkCreate(BaseModel):
    title: str
    url: HttpUrl
    description: str = ""

class BookmarkResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    title: str
    url: str
    description: str
    created_at: datetime
  

CRUD Router

  # app/routers/bookmarks.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Bookmark
from app.schemas import BookmarkCreate, BookmarkResponse

router = APIRouter(prefix="/bookmarks", tags=["bookmarks"])

@router.get("/", response_model=list[BookmarkResponse])
def list_bookmarks(db: Session = Depends(get_db)):
    return db.query(Bookmark).order_by(Bookmark.created_at.desc()).all()

@router.post("/", response_model=BookmarkResponse, status_code=201)
def create_bookmark(data: BookmarkCreate, db: Session = Depends(get_db)):
    bookmark = Bookmark(title=data.title, url=str(data.url), description=data.description)
    db.add(bookmark)
    db.commit()
    db.refresh(bookmark)
    return bookmark

@router.delete("/{bookmark_id}", status_code=204)
def delete_bookmark(bookmark_id: int, db: Session = Depends(get_db)):
    bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
    if not bookmark:
        raise HTTPException(status_code=404, detail="Bookmark not found")
    db.delete(bookmark)
    db.commit()
  

Tests

  # tests/test_bookmarks.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_and_list():
    response = client.post("/bookmarks/", json={
        "title": "Python Docs",
        "url": "https://docs.python.org",
    })
    assert response.status_code == 201
    assert response.json()["title"] == "Python Docs"

    response = client.get("/bookmarks/")
    assert response.status_code == 200
    assert len(response.json()) >= 1
  

Run: pytest tests/ -v

Run the Server

  uvicorn app.main:app --reload
# Visit http://127.0.0.1:8000/docs
  

Concepts Applied

Bonus Challenges

  1. Add tag support (many-to-many relationship)
  2. Add search by title/URL query parameter
  3. Add JWT authentication (Auth & Deployment)
  4. Dockerize the app (DevOps)
  5. Add pagination with skip and limit

This project mirrors real backend work — models, schemas, routes, tests, and auto-generated API docs.