On this page
article
Project: REST API with FastAPI
Build a complete bookmark manager REST API with FastAPI, SQLAlchemy, Pydantic validation, and pytest tests.
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
- Add tag support (many-to-many relationship)
- Add search by title/URL query parameter
- Add JWT authentication (Auth & Deployment)
- Dockerize the app (DevOps)
- Add pagination with
skipandlimit
This project mirrors real backend work — models, schemas, routes, tests, and auto-generated API docs.