By Md Esfer Abdus Sami
Product browsing with infinite scroll, URL-driven filters, SSG product pages,
a persisted cart, and JWT authentication — all backed by a real PostgreSQL database.
FunShop is a mini full-stack e-commerce application that demonstrates real-world practices: server-side rendering, static site generation, URL-driven filter state, infinite scrolling, JWT authentication through httpOnly cookies, and a globally persistent cart. Every API call hits a PostgreSQL database — pagination, search, and category filtering all run at the query level, not in memory.
- SSR Product List — server-rendered product grid with a sticky filter sidebar; search and category reflected in the URL so every view is shareable and browser-navigable
- Infinite Scrolling —
IntersectionObservertriggers next-page fetches as the user scrolls; initial data is rendered on the server - SSG Product Detail — all product pages statically generated at build time via
generateStaticParams, revalidated every hour (ISR) - Persistent Cart — Zustand store backed by
localStoragesurvives full page reloads and navigation; cart badge count updates globally from any page - JWT Auth via httpOnly Cookie — Next.js API routes proxy auth so the browser never holds the raw token; middleware protects
/cartand/checkout - Loading Skeletons — animated skeleton cards replace blank screens during data fetches
- SEO Metadata —
generateMetadataon product detail pages emits dynamic<title>and<meta description> - 29 pytest Tests — covers auth flows, product pagination, search, filtering, and 404 handling
╔══════════════════════════════════════════════════════════════════════════╗
║ BROWSER — Next.js 16 (port 3000) ║
║ ║
║ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ ║
║ │ Product List │ │ Product Detail │ │ Cart / Auth Pages │ ║
║ │ │ │ │ │ │ ║
║ │ SSR │ │ SSG (ISR 1h) │ │ Client-side │ ║
║ │ Infinite scroll │ │ Image gallery │ │ Zustand + localStorage│ ║
║ │ URL filter state│ │ Add to Cart │ │ httpOnly JWT cookie │ ║
║ │ Sticky sidebar │ │ Dynamic meta │ │ Middleware guard │ ║
║ └──────────────────┘ └──────────────────┘ └──────────────────────┘ ║
║ ║
║ React · TypeScript · Tailwind CSS · Zustand · Axios ║
╚═══════════════════════╦══════════════════════════════════════════════════╝
║ HTTPS REST / JSON
╔═══════════════════════╩══════════════════════════════════════════════════╗
║ BACKEND API — FastAPI (port 8000) ║
║ ║
║ ┌───────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ ║
║ │ /api/auth │ │ /api/products │ │ /api/categories │ ║
║ │ │ │ │ │ │ ║
║ │ POST /register │ │ GET / (paginated) │ │ GET / │ ║
║ │ POST /login │ │ GET /{id} │ │ slug · name │ ║
║ │ GET /me │ │ ?search · ?category │ │ │ ║
║ │ JWT HS256 │ │ ?page · ?limit │ │ │ ║
║ └───────────────────┘ └──────────────────────┘ └──────────────────┘ ║
║ ║
║ SQLAlchemy 2 (async) · Alembic · Pydantic · python-jose · bcrypt ║
╚═══════════════════════╦══════════════════════════════════════════════════╝
║ asyncpg (SQL)
╔═══════════════════════╩══════════════════════════════════════════════════╗
║ PostgreSQL 16 (port 5432) ║
║ ║
║ Tables ║
║ ├── users id · name · email · hashed_password ║
║ ├── categories id · slug · name ║
║ └── products id · title · description · price · stock · rating ║
║ thumbnail · images · category_id (FK) ║
╚══════════════════════════════════════════════════════════════════════════╝
| Service | Stack | Port | Responsibility |
|---|---|---|---|
| Frontend | Next.js 16, TypeScript, Tailwind CSS, Zustand | 3000 | SSR/SSG pages, infinite scroll, cart state, auth UI |
| Backend | FastAPI, SQLAlchemy 2, Alembic, python-jose | 8000 | REST API, JWT auth, DB-level pagination/filtering |
| Database | PostgreSQL 16 | 5432 | Users, categories, products |
/products SSR ← server reads searchParams, renders filtered grid
│
▼ click product
/products/[id] SSG ← statically generated at build; ISR revalidates hourly
│
▼ add to cart
/cart CSR ← Zustand (localStorage) — no server round-trip needed
│
▼ checkout
/checkout CSR ← form-only, protected by middleware JWT check
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/products |
Paginated list — page, limit, search, category |
GET |
/api/products/{id} |
Single product; 404 when not found |
GET |
/api/categories |
All categories (slug + name) |
POST |
/api/auth/register |
Create account — returns JWT |
POST |
/api/auth/login |
Login — returns JWT |
GET |
/api/auth/me |
Authenticated user info |
Pagination, search, and category filtering run at the database query level — ILIKE for search, LIMIT/OFFSET for pages, indexed category_id for filtering. Full interactive docs at http://localhost:8000/docs (Swagger UI) and /redoc.
| Route | Rendering | Key Features |
|---|---|---|
/products |
SSR | Infinite scroll, sticky sidebar, URL filter state (?search=&category=) |
/products/[id] |
SSG (ISR 1h) | Image gallery, Add to Cart, generateMetadata |
/cart |
Client | Quantity controls, remove items, subtotal / total — JWT protected |
/checkout |
Client | Checkout form — JWT protected |
/login |
Client | JWT login |
/register |
Client | Registration |
/profile |
Client | Authenticated user profile |
| Dependency | Version |
|---|---|
| Python | 3.11+ |
| Node.js | 18+ |
| PostgreSQL | 14+ |
| uv | latest |
git clone <repo-url>
cd FunshopCREATE DATABASE funshope;cd backend
cp .env.example .envEdit .env:
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=your-long-random-secret-key
DATABASE_URL=postgresql://postgres:password@localhost:5432/funshope
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
FRONTEND_URL=http://localhost:3000uv syncuv run alembic upgrade headuv run python scripts/seed.pyFetches 100+ products from DummyJSON as a one-time import and persists them to PostgreSQL. All runtime requests hit your own API — DummyJSON is never contacted again.
uv run uvicorn app.main:app --reload
# → http://localhost:8000
# → http://localhost:8000/docscd ../frontendCreate .env.local:
NEXT_PUBLIC_API_URL=http://localhost:8000npm install
npm run dev
# → http://localhost:3000Open http://localhost:3000, register an account, and start browsing.
Spins up PostgreSQL + FastAPI + Next.js with a single command. Migrations and seeding run automatically on first start.
cd Funshop
docker compose up --build| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| Backend API | http://localhost:8000 |
| Swagger docs | http://localhost:8000/docs |
Stop and remove volumes:
docker compose down -vNote:
NEXT_PUBLIC_API_URLis baked into the frontend bundle at build time. If you change the API port, rebuild the frontend image withdocker compose build frontend.
cd backend
uv run pytest29 tests covering auth (register, login, JWT), products (pagination, search, filtering, 404), and categories.
Funshop/
├── backend/
│ ├── app/
│ │ ├── api/ # Route handlers (products, categories, auth)
│ │ ├── core/ # Config, database, security
│ │ ├── models/ # SQLAlchemy models
│ │ ├── schemas/ # Pydantic request/response schemas
│ │ └── services/ # Business logic
│ ├── migrations/ # Alembic migration files
│ ├── scripts/
│ │ └── seed.py # Seeds 100+ products from DummyJSON
│ └── tests/ # pytest test suite (29 tests)
├── frontend/
│ └── src/
│ ├── app/ # Next.js pages (App Router)
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom hooks (infinite scroll, filters)
│ ├── lib/ # Axios client
│ ├── store/ # Zustand cart store
│ └── types/ # TypeScript types
├── assests/ # Logo and screenshots
├── docker-compose.yml
└── README.md
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
Yes | — | JWT signing secret |
DATABASE_URL |
Yes | — | PostgreSQL connection string |
ALGORITHM |
No | HS256 |
JWT signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
No | 30 |
JWT token lifetime |
FRONTEND_URL |
No | http://localhost:3000 |
Allowed CORS origin |
NEXT_PUBLIC_API_URL |
Yes (frontend) | — | Backend base URL for the frontend bundle |
- The frontend proxies auth calls through Next.js API routes (
/api/auth/*) so the JWT access token is stored in an httpOnly cookie — the browser never holds the raw token directly. - A second readable cookie (
auth_user) holds only the display name for the header — no sensitive data. - The backend CORS origin is configured via
FRONTEND_URLin.env, not hardcoded. - Product images are served from DummyJSON CDN URLs stored in the database at seed time. The Next.js image domain allowlist in
next.config.tscovers these origins.
- Checkout is UI-only — there is no payment processing or order persistence.
- The seed script is idempotent (safe to run multiple times) but requires an internet connection on first run to fetch from DummyJSON.
- JWT tokens expire in 30 minutes. There is no refresh token flow; users are redirected to login on expiry.
- Stock levels are seeded from DummyJSON and are not decremented on cart actions.
MIT
