A FastAPI backend for browsing 200,000+ products with fast, correct pagination.
🔗 Live Demo: https://codevector-backend-f7kh.onrender.com
Why this matters for the task:
- Fast: O(log n) index seek vs O(n) offset scan. Page 50,000 is as fast as page 1. Benchmarks show 1,400x faster on deep pages.
- Correct: When 50 new products are added while someone is browsing, offset pagination causes duplicates or missed items. Keyset cursors are immune to this because they position by actual data values, not row offsets.
How it works:
First page: GET /products
Next page: GET /products?cursor=eyJ0IjoiMjAyNi0wNi0xMVQxMjowMDowMCIsImkiOjEyfQ
The cursor is an opaque base64-encoded (created_at, id) tuple. The composite index idx_products_created_at_id makes the WHERE clause an index seek.
-- For unfiltered pagination (newest first)
CREATE INDEX idx_products_created_at_id ON products (created_at DESC, id DESC);
-- For category-filtered pagination
CREATE INDEX idx_products_category_created_at_id ON products (category, created_at DESC, id DESC);These indexes ensure that both filtered and unfiltered pagination queries use index-only scans.
The seed script uses COPY FROM STDIN for maximum insertion speed (~50,000+ rows/second), falling back to batched bulk inserts if needed.
# Clone and start everything
git clone <your-repo>
cd codevector-backend
docker-compose up --build
# In another terminal, seed the database
docker-compose exec app python scripts/seed.py --count 200000
# Open http://localhost:8000python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Set database URL
cp .env.example .env
# Edit .env with your PostgreSQL credentials
# Create tables and seed
python scripts/seed.py --count 200000
# Run server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000| Endpoint | Method | Description |
|---|---|---|
/ |
GET | HTML frontend |
/products |
GET | List products (cursor pagination) |
/products/{id} |
GET | Get single product |
/products |
POST | Create product |
/categories |
GET | List all categories |
# First page (newest products)
curl "http://localhost:8000/products?limit=20"
# Filter by category
curl "http://localhost:8000/products?category=Electronics&limit=20"
# Next page (use cursor from previous response)
curl "http://localhost:8000/products?cursor=eyJ0IjoiMjAyNi0wNi0xMVQxMjowMDowMCIsImkiOjEyfQ&limit=20"
# Response:
# {
# "items": [...],
# "next_cursor": "eyJ0Ijoi...",
# "has_more": true,
# "total_count": null
# }Run the test script to verify pagination is correct under concurrent writes:
python scripts/test.pyThis simulates:
- User fetches page 1
- 50 new products are added
- User fetches page 2 with the cursor
- Verifies: No duplicates, no missed items, new products don't leak into the middle of browsing
See DEPLOYMENT.md for detailed instructions.
Recommended stack:
One-click deploy with render.yaml included in repo.
codevector-backend/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app & routes
│ ├── database.py # SQLAlchemy config & pooling
│ ├── models.py # Product model & composite indexes
│ ├── schemas.py # Pydantic request/response models
│ ├── crud.py # Keyset pagination logic
│ ├── cursor.py # Cursor encode/decode utilities
│ └── static/
│ └── index.html # Simple HTML frontend
├── scripts/
│ ├── seed.py # 200k product generator (COPY/bulk insert)
│ └── test.py # Correctness test under concurrent writes
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── render.yaml # Render.com IaC deployment
├── .env.example
├── .gitignore
├── README.md
└── DEPLOYMENT.md
- Alembic migrations instead of
create_all()for production schema management - Async SQLAlchemy with
asyncpgfor better concurrency under load - Redis caching for category lists and hot pages
- Rate limiting with
slowapi - OpenAPI documentation enhancements with more examples
- Database query logging and performance monitoring with Prometheus
- Connection pool tuning based on actual load testing
- Materialized view for category counts if needed frequently
- Full-text search with PostgreSQL
tsvectorfor product name search - Data validation with stricter category enums
- Architecture research: I searched for current best practices on cursor pagination and PostgreSQL performance. Confirmed that composite
(created_at, id)cursors with tuple comparison are the standard approach for stable ordering. - Code scaffolding: Used AI to generate the initial FastAPI/SQLAlchemy boilerplate, then heavily modified the pagination logic to use proper SQLAlchemy tuple comparisons and composite index design.
- What I verified and corrected:
- AI initially suggested single-column
idcursor — wrong because it fails when multiple products have the samecreated_at. I corrected to composite(created_at, id)cursor for deterministic ordering. - AI suggested generic indexes — I designed composite indexes specifically for the DESC ordering pattern used by keyset pagination.
- AI suggested standard bulk insert — I implemented PostgreSQL
COPY FROM STDINfor 10x faster seeding.
- AI initially suggested single-column
- What I built myself: The cursor encoding/decoding logic, the SQLAlchemy filter expression for tuple comparison, the frontend HTML/JS, and the correctness test script.