Files
dutch-food-price-tracker/README.md
T
Jonathan 486749a890 Initial project scaffold
Full-stack Dutch supermarket price tracker with FastAPI backend,
PostgreSQL/SQLAlchemy, Albert Heijn scraper, and Next.js frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:27:24 +02:00

4.0 KiB

Dutch Food Price Tracker

Track Dutch supermarket food prices over time. Currently supports Albert Heijn.

Stack

Layer Tech
Backend API Python FastAPI + SQLAlchemy + Alembic
Database PostgreSQL 16
Scraper httpx + AH anonymous token auth
Frontend Next.js 14 + TypeScript + Tailwind CSS + Recharts
Dev infra Docker Compose

Quick start (Docker)

git clone <repo> && cd dutch-food-price-tracker

# Build and start all services
docker compose up --build -d

# Run DB migrations (tables are also auto-created on backend start)
docker compose exec backend alembic upgrade head

# Scrape Albert Heijn
docker compose exec backend python cli.py scrape-ah \
  --query melk \
  --query brood \
  --query kaas \
  --query yoghurt

Local development (without Docker)

Backend

cd backend
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt

# Needs a running PostgreSQL instance
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/food_prices

# Create tables
alembic upgrade head
# or: python -c "from app.database import engine; from app.models import Base; Base.metadata.create_all(engine)"

# Start API server
uvicorn app.main:app --reload

Frontend

cd frontend
npm install

# Point at the local backend
echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local

npm run dev

CLI reference

# Scrape one or more queries
python cli.py scrape-ah --query "melk" --query "brood"

# Run via Docker
docker compose exec backend python cli.py scrape-ah --query melk

API endpoints

Method Path Description
GET /api/products?search=melk Search products
GET /api/products/{id} Product detail
GET /api/products/{id}/prices Full price history
GET /api/prices/cheapest?date=2024-01-15 Cheapest per product for a day
GET /api/stores List stores
GET /api/scrape-runs List recent scrape runs

Project structure

dutch-food-price-tracker/
├── backend/
│   ├── app/
│   │   ├── main.py          # FastAPI app + CORS
│   │   ├── models.py        # SQLAlchemy ORM models
│   │   ├── schemas.py       # Pydantic request/response schemas
│   │   ├── database.py      # Engine, session, Base
│   │   ├── config.py        # Pydantic settings
│   │   ├── routers/         # products, stores, prices, scrape_runs
│   │   └── scrapers/
│   │       └── albert_heijn.py  # Token auth + product search
│   ├── alembic/             # DB migration history
│   ├── cli.py               # Click CLI (scrape-ah)
│   └── requirements.txt
├── frontend/
│   └── src/
│       ├── app/
│       │   ├── page.tsx              # Product search
│       │   ├── products/[id]/page.tsx # Detail + price chart
│       │   └── cheapest/page.tsx      # Daily cheapest overview
│       ├── components/
│       │   ├── Nav.tsx
│       │   ├── ProductCard.tsx
│       │   └── PriceChart.tsx        # Recharts line chart
│       └── lib/api.ts               # Typed API client
├── seed/                    # Historical CSV/JSON import datasets
├── docker-compose.yml
└── .env.example

Adding a new store

  1. Create backend/app/scrapers/<store_slug>.py
  2. Implement scrape_query(db: Session, query: str) -> ScrapeRun
  3. Add a @cli.command in backend/cli.py
  4. Insert a Store row with the new slug

Data model

  • stores — one row per chain (Albert Heijn, Jumbo, …)
  • products — one row per store SKU; keyed by (store_id, external_id)
  • scrape_runs — one row per CLI invocation / query
  • price_snapshots — append-only price observations (cents, UTC timestamp)