├── .gitignore ├── app ├── backend │ ├── .python-version │ ├── app │ │ ├── __init__.py │ │ ├── data │ │ │ ├── __init__.py │ │ │ └── seed_products.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ └── products.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── config.py │ │ │ └── logging_config.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── error.py │ │ │ └── product.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ └── product_service.py │ │ └── main.py │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_products_basic.py │ │ └── test_products_filtering.py │ ├── run_api.py │ ├── .gitignore │ └── pyproject.toml └── frontend │ ├── bunfig.toml │ ├── src │ ├── lib │ │ ├── utils.ts │ │ ├── logger.ts │ │ └── api-client.ts │ ├── react.svg │ ├── index.html │ ├── components │ │ ├── ui │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── card.tsx │ │ │ ├── button.tsx │ │ │ ├── form.tsx │ │ │ └── select.tsx │ │ ├── ProductGrid.tsx │ │ └── ProductCard.tsx │ ├── frontend.tsx │ ├── index.css │ ├── index.tsx │ ├── types │ │ ├── error.ts │ │ └── product.ts │ ├── APITester.tsx │ ├── logo.svg │ └── App.tsx │ ├── bun-env.d.ts │ ├── tsconfig.json │ ├── components.json │ ├── .gitignore │ ├── package.json │ ├── biome.json │ ├── styles │ └── globals.css │ ├── build.ts │ └── bun.lock ├── rules-and-commands ├── commands │ ├── execute.md │ ├── prime.md │ ├── start-server.md │ ├── commit.md │ ├── prp-review.md │ ├── noqa.md │ └── planning.md └── rules │ └── CLAUDE.md ├── tasks ├── TASK1.md └── TASK2.md ├── SETUP_GUIDE.md ├── exercises ├── exercise_1.md ├── exercise_2.md └── exercise_3.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | -------------------------------------------------------------------------------- /app/backend/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /app/backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | """Product catalog API application package.""" 2 | -------------------------------------------------------------------------------- /app/backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for the product catalog API.""" 2 | -------------------------------------------------------------------------------- /app/backend/app/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Sample data for the product catalog.""" 2 | -------------------------------------------------------------------------------- /app/backend/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | """API route handlers for the product catalog.""" 2 | -------------------------------------------------------------------------------- /app/backend/app/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Core application configuration and utilities.""" 2 | -------------------------------------------------------------------------------- /app/backend/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Data models for the product catalog API.""" 2 | -------------------------------------------------------------------------------- /app/backend/app/services/__init__.py: -------------------------------------------------------------------------------- 1 | """Business logic services for the product catalog API.""" 2 | -------------------------------------------------------------------------------- /app/frontend/bunfig.toml: -------------------------------------------------------------------------------- 1 | [serve.static] 2 | plugins = ["bun-plugin-tailwind"] 3 | env = "BUN_PUBLIC_*" 4 | -------------------------------------------------------------------------------- /app/frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /app/frontend/src/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/backend/run_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Development server runner for the product catalog API. 3 | 4 | Run this script to start the development server: 5 | python run_api.py 6 | """ 7 | 8 | import uvicorn 9 | 10 | if __name__ == "__main__": 11 | uvicorn.run( 12 | "app.main:app", 13 | host="0.0.0.0", 14 | port=8000, 15 | reload=True, 16 | log_level="info" 17 | ) 18 | -------------------------------------------------------------------------------- /app/frontend/bun-env.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by `bun init` 2 | 3 | declare module "*.svg" { 4 | /** 5 | * A path to the SVG file 6 | */ 7 | const path: `${string}.svg`; 8 | export = path; 9 | } 10 | 11 | declare module "*.module.css" { 12 | /** 13 | * A record of class names to their corresponding CSS module classes 14 | */ 15 | const classes: { readonly [key: string]: string }; 16 | export = classes; 17 | } 18 | -------------------------------------------------------------------------------- /app/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | 8 | # Virtual environments 9 | .venv/ 10 | venv/ 11 | ENV/ 12 | env/ 13 | 14 | # UV 15 | uv.lock 16 | 17 | # Testing 18 | .pytest_cache/ 19 | .coverage 20 | htmlcov/ 21 | 22 | # IDEs 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | *~ 28 | 29 | # OS 30 | .DS_Store 31 | Thumbs.db 32 | 33 | .ruff 34 | .ruff_cache 35 | .ruff_cache/ 36 | -------------------------------------------------------------------------------- /app/frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bun + React 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /app/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": true, 5 | 6 | // Bundler mode 7 | "moduleResolution": "bundler", 8 | "module": "Preserve", 9 | "allowImportingTsExtensions": true, 10 | "verbatimModuleSyntax": true, 11 | "noEmit": true, 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | } 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"], 19 | "exclude": ["dist", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /app/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /app/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import type * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Label({ className, ...props }: React.ComponentProps) { 9 | return ( 10 | 18 | ); 19 | } 20 | 21 | export { Label }; 22 | -------------------------------------------------------------------------------- /app/backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pytest configuration and shared test fixtures. 3 | 4 | This module provides reusable test fixtures for all test files. 5 | """ 6 | 7 | import pytest 8 | from fastapi.testclient import TestClient 9 | from app.main import app 10 | 11 | 12 | @pytest.fixture 13 | def test_client() -> TestClient: 14 | """ 15 | Provide a FastAPI TestClient for making HTTP requests in tests. 16 | 17 | Returns: 18 | TestClient instance configured with the FastAPI app 19 | 20 | Example: 21 | def test_get_products(test_client): 22 | response = test_client.get("/api/products") 23 | assert response.status_code == 200 24 | """ 25 | return TestClient(app) 26 | -------------------------------------------------------------------------------- /app/frontend/src/frontend.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is the entry point for the React app, it sets up the root 3 | * element and renders the App component to the DOM. 4 | * 5 | * It is included in `src/index.html`. 6 | */ 7 | 8 | import { StrictMode } from "react"; 9 | import { createRoot } from "react-dom/client"; 10 | import { App } from "./App"; 11 | 12 | const elem = document.getElementById("root")!; 13 | const app = ( 14 | 15 | 16 | 17 | ); 18 | 19 | if (import.meta.hot) { 20 | // With hot module reloading, `import.meta.hot.data` is persisted. 21 | const root = (import.meta.hot.data.root ??= createRoot(elem)); 22 | root.render(app); 23 | } else { 24 | // The hot module reloading API is not available in production. 25 | createRoot(elem).render(app); 26 | } 27 | -------------------------------------------------------------------------------- /app/backend/app/core/config.py: -------------------------------------------------------------------------------- 1 | """Application configuration settings.""" 2 | 3 | from pydantic_settings import BaseSettings 4 | 5 | 6 | class ApplicationSettings(BaseSettings): 7 | """ 8 | Application-wide configuration settings. 9 | 10 | These settings can be overridden via environment variables. 11 | For example, LOG_LEVEL=DEBUG will set the logging level to DEBUG. 12 | 13 | Attributes: 14 | application_name: Display name of the application 15 | application_version: Semantic version number 16 | log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) 17 | enable_cors: Whether to enable CORS (Cross-Origin Resource Sharing) 18 | """ 19 | 20 | application_name: str = "Product Catalog API" 21 | application_version: str = "0.1.0" 22 | log_level: str = "INFO" 23 | enable_cors: bool = True 24 | 25 | 26 | # Global settings instance 27 | settings = ApplicationSettings() 28 | -------------------------------------------------------------------------------- /rules-and-commands/commands/execute.md: -------------------------------------------------------------------------------- 1 | # Execute PRP Plan 2 | 3 | Implement a feature plan from the PRPs directory by following its Step by Step Tasks section. 4 | 5 | ## Variables 6 | 7 | Plan file: $ARGUMENTS 8 | 9 | ## Instructions 10 | 11 | - Read the entire plan file carefully 12 | - Execute **every step** in the "Step by Step Tasks" section in order, top to bottom 13 | - Follow the "Testing Strategy" to create proper unit and integration tests 14 | - Complete all "Validation Commands" at the end 15 | - Ensure all linters pass and all tests pass before finishing 16 | - Follow CLAUDE.md guidelines for type safety, logging, and docstrings 17 | 18 | ## When done 19 | 20 | - Move the PRP file to the completed directory in PRPs/features/completed 21 | 22 | ## Report 23 | 24 | - Summarize completed work in a concise bullet point list 25 | - Show files and lines changed: `git diff --stat` 26 | - Confirm all validation commands passed 27 | - Note any deviations from the plan (if any) 28 | -------------------------------------------------------------------------------- /app/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import "../styles/globals.css"; 2 | 3 | @layer base { 4 | :root { 5 | @apply font-sans; 6 | } 7 | 8 | body { 9 | @apply grid place-items-center min-w-[320px] min-h-screen relative m-0 bg-background text-foreground; 10 | } 11 | } 12 | 13 | /* cool Bun background animation 😎 */ 14 | body::before { 15 | content: ""; 16 | position: fixed; 17 | inset: 0; 18 | z-index: -1; 19 | opacity: 0.05; 20 | background: url("./logo.svg"); 21 | background-size: 256px; 22 | transform: rotate(-12deg) scale(1.35); 23 | animation: slide 30s linear infinite; 24 | pointer-events: none; 25 | } 26 | 27 | @keyframes slide { 28 | from { 29 | background-position: 0 0; 30 | } 31 | to { 32 | background-position: 256px 224px; 33 | } 34 | } 35 | 36 | @keyframes spin { 37 | from { 38 | transform: rotate(0); 39 | } 40 | to { 41 | transform: rotate(360deg); 42 | } 43 | } 44 | 45 | @media (prefers-reduced-motion) { 46 | *, 47 | ::before, 48 | ::after { 49 | animation: none !important; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { serve } from "bun"; 2 | import index from "./index.html"; 3 | 4 | const server = serve({ 5 | routes: { 6 | // Serve index.html for all unmatched routes. 7 | "/*": index, 8 | 9 | "/api/hello": { 10 | async GET(req) { 11 | return Response.json({ 12 | message: "Hello, world!", 13 | method: "GET", 14 | }); 15 | }, 16 | async PUT(req) { 17 | return Response.json({ 18 | message: "Hello, world!", 19 | method: "PUT", 20 | }); 21 | }, 22 | }, 23 | 24 | "/api/hello/:name": async (req) => { 25 | const name = req.params.name; 26 | return Response.json({ 27 | message: `Hello, ${name}!`, 28 | }); 29 | }, 30 | }, 31 | 32 | development: process.env.NODE_ENV !== "production" && { 33 | // Enable browser hot reloading in development 34 | hmr: true, 35 | 36 | // Echo console logs from the browser to the server 37 | console: true, 38 | }, 39 | }); 40 | 41 | console.log(`🚀 Server running at ${server.url}`); 42 | -------------------------------------------------------------------------------- /tasks/TASK1.md: -------------------------------------------------------------------------------- 1 | # [FEAT-1234] Add Product Filtering to Catalog API 2 | 3 | ## Description 4 | 5 | Users need to filter and search products in the catalog API. Currently `GET /api/products` returns all 30 products without filtering capabilities. 6 | 7 | ## Requirements 8 | 9 | Add filtering and search capabilities to `GET /api/products`: 10 | 11 | - **Price filtering**: Support minimum and maximum price filters 12 | - **Category filtering**: Allow filtering by product category 13 | - **Keyword search**: Search product names and descriptions 14 | - **Sorting**: Enable sorting by price or name (both directions) 15 | 16 | All filters should be optional and work together when combined. 17 | 18 | ## Acceptance Criteria 19 | 20 | - [ ] All filtering tests pass 21 | - [ ] Invalid inputs return appropriate HTTP 400 errors 22 | - [ ] Backwards compatible (no filters = all products) 23 | - [ ] Follows existing code patterns and conventions 24 | 25 | ## Technical Notes 26 | 27 | - Validate query parameters 28 | - Use appropriate types for monetary values 29 | - Log filter operations 30 | 31 | ## Definition of Done 32 | 33 | - All acceptance criteria met 34 | - All Tests passing 35 | `uv run pytest` 36 | -------------------------------------------------------------------------------- /rules-and-commands/commands/prime.md: -------------------------------------------------------------------------------- 1 | # Prime 2 | 3 | Execute the following sections to understand the codebase before starting new work, then summarize your understanding. 4 | 5 | ## Run 6 | 7 | - List all tracked files: `git ls-files` 8 | - Show project structure: `tree -I '.venv|__pycache__|*.pyc|.pytest_cache|.mypy_cache|.ruff_cache' -L 3` 9 | 10 | ## Read 11 | 12 | - `CLAUDE.md` - Core project instructions, principles, logging rules, testing requirements 13 | - `app/backend/README.md` - Project overview and setup (if exists) 14 | - `app/frontend/README.md` - Project overview and setup (if exists) 15 | 16 | - Identify core files in both backend and frontend and read them 17 | 18 | ## Report 19 | 20 | Provide a concise summary of: 21 | 22 | 1. **Project Purpose**: What this application does 23 | 2. **Architecture**: Key patterns (vertical slice, FastAPI + Pydantic AI) 24 | 3. **Core Principles**: TYPE SAFETY, KISS, YAGNI 25 | 4. **Tech Stack**: Main dependencies and tools 26 | 5. **Key Requirements**: Logging, testing, type annotations 27 | 6. **Current State**: What's implemented 28 | 29 | Keep the summary brief (5-10 bullet points) and focused on what you need to know to contribute effectively. 30 | -------------------------------------------------------------------------------- /rules-and-commands/commands/start-server.md: -------------------------------------------------------------------------------- 1 | # Start Servers 2 | 3 | Start both the FastAPI backend and React frontend development servers with hot reload. 4 | 5 | ## Run 6 | 7 | ### Run in the background with bash tool 8 | 9 | - Ensure you are in the right PWD 10 | - Use the Bash tool to run the servers in the background so you can read the shell outputs 11 | - IMPORTANT: run `git ls-files` first so you know where directories are located before you start 12 | 13 | ### Backend Server (FastAPI) 14 | 15 | - Navigate to backend: `cd app/backend` 16 | - Start server in background: `uv sync && uv run python run_api.py` 17 | - Wait 2-3 seconds for startup 18 | - Test health endpoint: `curl http://localhost:8000/health` 19 | - Test products endpoint: `curl http://localhost:8000/api/products` 20 | 21 | ### Frontend Server (Bun + React) 22 | 23 | - Navigate to frontend: `cd ../app/frontend` 24 | - Start server in background: `bun install && bun dev` 25 | - Wait 2-3 seconds for startup 26 | - Frontend should be accessible at `http://localhost:3000` 27 | 28 | ## Report 29 | 30 | - Confirm backend is running on `http://localhost:8000` 31 | - Confirm frontend is running on `http://localhost:3000` 32 | - Show the health check response from backend 33 | - Mention: "Backend logs will show structured JSON logging for all requests" 34 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 16 | ); 17 | } 18 | 19 | export { Input }; 20 | -------------------------------------------------------------------------------- /app/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product-catalog-frontend", 3 | "version": "0.1.0", 4 | "description": "E-commerce product catalog frontend - Module 1 Exercise", 5 | "private": true, 6 | "type": "module", 7 | "main": "src/index.tsx", 8 | "module": "src/index.tsx", 9 | "scripts": { 10 | "dev": "bun --hot src/index.tsx", 11 | "start": "NODE_ENV=production bun src/index.tsx", 12 | "build": "bun run build.ts", 13 | "lint": "bunx biome lint src/", 14 | "lint:fix": "bunx biome lint --write src/", 15 | "format": "bunx biome format src/", 16 | "format:fix": "bunx biome format --write src/", 17 | "check": "bunx biome check src/", 18 | "check:fix": "bunx biome check --write src/", 19 | "ci": "bunx biome ci src/" 20 | }, 21 | "dependencies": { 22 | "@hookform/resolvers": "^4.1.0", 23 | "@radix-ui/react-label": "^2.1.2", 24 | "@radix-ui/react-select": "^2.1.6", 25 | "@radix-ui/react-slot": "^1.1.2", 26 | "bun-plugin-tailwind": "^0.0.14", 27 | "class-variance-authority": "^0.7.1", 28 | "clsx": "^2.1.1", 29 | "lucide-react": "^0.475.0", 30 | "react": "^19", 31 | "react-dom": "^19", 32 | "react-hook-form": "^7.54.2", 33 | "tailwind-merge": "^3.0.1", 34 | "tailwindcss": "^4.0.6", 35 | "tailwindcss-animate": "^1.0.7", 36 | "zod": "^3.24.2" 37 | }, 38 | "devDependencies": { 39 | "@biomejs/biome": "2.2.6", 40 | "@types/bun": "latest", 41 | "@types/react": "^19", 42 | "@types/react-dom": "^19" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
12 | ); 13 | } 14 | 15 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 16 | return
; 17 | } 18 | 19 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 20 | return ( 21 |
22 | ); 23 | } 24 | 25 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 26 | return
; 27 | } 28 | 29 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 30 | return
; 31 | } 32 | 33 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 34 | return
; 35 | } 36 | 37 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; 38 | -------------------------------------------------------------------------------- /app/backend/app/services/product_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Product service containing business logic for product operations. 3 | 4 | This service layer separates business logic from API routing logic, 5 | making the code more testable and maintainable. 6 | """ 7 | 8 | from app.core.logging_config import StructuredLogger 9 | from app.data.seed_products import get_seed_products 10 | from app.models.product import Product 11 | 12 | # Initialize structured logger for this module 13 | logger = StructuredLogger(__name__) 14 | 15 | # In-memory product storage (in a real app, this would be a database) 16 | _PRODUCTS_DATABASE: list[Product] = get_seed_products() 17 | 18 | 19 | def get_all_products() -> list[Product]: 20 | """ 21 | Retrieve all products from the catalog. 22 | 23 | This function returns all available products without any filtering. 24 | It logs the operation for debugging and monitoring purposes. 25 | 26 | Returns: 27 | List of all Product objects in the catalog 28 | 29 | Example: 30 | >>> products = get_all_products() 31 | >>> len(products) 32 | 30 33 | >>> products[0].product_name 34 | 'Wireless Bluetooth Mouse' 35 | """ 36 | logger.info( 37 | "retrieving_all_products", total_products_in_database=len(_PRODUCTS_DATABASE), operation="get_all_products" 38 | ) 39 | 40 | logger.info( 41 | "products_retrieved_successfully", products_returned=len(_PRODUCTS_DATABASE), operation="get_all_products" 42 | ) 43 | 44 | return _PRODUCTS_DATABASE 45 | -------------------------------------------------------------------------------- /rules-and-commands/commands/commit.md: -------------------------------------------------------------------------------- 1 | # Create Git Commit 2 | 3 | Create an atomic git commit with a properly formatted commit message following best practices for the uncommited changes or these specific files if specified. 4 | 5 | Specific files (skip if not specified): 6 | 7 | - File 1: $1 8 | - File 2: $2 9 | - File 3: $3 10 | - File 4: $4 11 | - File 5: $5 12 | 13 | ## Instructions 14 | 15 | **Commit Message Format:** 16 | 17 | - Use conventional commits: `: ` 18 | - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` 19 | - Present tense (e.g., "add", "fix", "update", not "added", "fixed", "updated") 20 | - 50 characters or less for the subject line 21 | - Lowercase subject line 22 | - No period at the end 23 | - Be specific and descriptive 24 | 25 | **Examples:** 26 | 27 | - `feat: add web search tool with structured logging` 28 | - `fix: resolve type errors in middleware` 29 | - `test: add unit tests for config module` 30 | - `docs: update CLAUDE.md with testing guidelines` 31 | - `refactor: simplify logging configuration` 32 | - `chore: update dependencies` 33 | 34 | **Atomic Commits:** 35 | 36 | - One logical change per commit 37 | - If you've made multiple unrelated changes, consider splitting into separate commits 38 | - Commit should be self-contained and not break the build 39 | 40 | **IMPORTANT** 41 | 42 | - NEVER mention claude code, anthropic, co authored by or anything similar in the commit messages 43 | 44 | ## Run 45 | 46 | 1. Review changes: `git diff HEAD` 47 | 2. Check status: `git status` 48 | 3. Stage changes: `git add -A` 49 | 4. Create commit: `git commit -m ": "` 50 | 51 | ## Report 52 | 53 | - Output the commit message used 54 | - Confirm commit was successful with commit hash 55 | - List files that were committed 56 | -------------------------------------------------------------------------------- /app/backend/app/models/error.py: -------------------------------------------------------------------------------- 1 | """Error response models for consistent API error handling.""" 2 | 3 | from datetime import UTC, datetime 4 | from typing import Any 5 | 6 | from pydantic import BaseModel, Field 7 | 8 | 9 | class ErrorResponse(BaseModel): 10 | """ 11 | Standard error response format for all API errors. 12 | 13 | This model ensures consistent error messaging across the entire API, 14 | making it easier for both humans and AI to understand what went wrong. 15 | 16 | Attributes: 17 | error_code: Machine-readable error identifier (e.g., 'invalid_price_range') 18 | error_message: Human-readable error message for end users 19 | error_details: Optional dictionary with additional debugging context 20 | timestamp_utc: ISO 8601 formatted timestamp when the error occurred 21 | 22 | Examples: 23 | >>> ErrorResponse( 24 | ... error_code="invalid_price_range", 25 | ... error_message="Minimum price cannot exceed maximum price", 26 | ... error_details={"min_price": "100.00", "max_price": "50.00"} 27 | ... ) 28 | """ 29 | 30 | error_code: str = Field( 31 | ..., 32 | description="Machine-readable error code for programmatic handling", 33 | examples=["invalid_price_range", "product_not_found", "validation_error"], 34 | min_length=1, 35 | max_length=100, 36 | ) 37 | 38 | error_message: str = Field( 39 | ..., 40 | description="Human-readable error message suitable for displaying to end users", 41 | min_length=1, 42 | max_length=500, 43 | ) 44 | 45 | error_details: dict[str, Any] | None = Field( 46 | default=None, description="Additional context about the error for debugging purposes" 47 | ) 48 | 49 | timestamp_utc: str = Field( 50 | default_factory=lambda: datetime.now(UTC).isoformat(), 51 | description="ISO 8601 timestamp when the error occurred (UTC timezone)", 52 | ) 53 | -------------------------------------------------------------------------------- /tasks/TASK2.md: -------------------------------------------------------------------------------- 1 | # [FEAT-1235] Add Product Filtering UI to Frontend 2 | 3 | ## Description 4 | 5 | Backend filtering capabilities are now available on `GET /api/products` with query parameters. Users need a frontend interface to interact with these filters and see filtered results. 6 | 7 | ## Requirements 8 | 9 | Build a product filtering interface that connects to the backend filtering API: 10 | 11 | - **Price range controls**: Allow users to set minimum and maximum price filters 12 | - **Category selector**: Enable filtering by product category 13 | - **Search input**: Provide keyword search for product names and descriptions 14 | - **Sort controls**: Allow users to sort results by price or name 15 | - **Filter management**: Provide ability to apply and clear filters 16 | 17 | The interface should handle loading states, empty results, and validation errors appropriately. 18 | 19 | ## Acceptance Criteria 20 | 21 | - [ ] Price filtering interface works and sends correct query parameters 22 | - [ ] Category selection filters products correctly 23 | - [ ] Search input filters products by keyword 24 | - [ ] Sort options reorder products as expected 25 | - [ ] Multiple filters work together (combined filtering) 26 | - [ ] Clear filters returns to showing all products 27 | - [ ] Validation errors are displayed to users 28 | - [ ] Empty state shown when no products match filters 29 | - [ ] Loading state shown during filter operations 30 | - [ ] All filter interactions logged to console (structured JSON) 31 | 32 | ## Technical Notes 33 | 34 | - Update API client to accept filter parameters 35 | - Build query string from filter values 36 | - Use React Hook Form and Zod for form validation 37 | - Follow existing logging patterns (structured JSON) 38 | - Use existing shadcn components where possible 39 | 40 | ## Definition of Done 41 | 42 | - All acceptance criteria met 43 | - Backend API called with correct query parameters 44 | - Follows existing code patterns and conventions 45 | - Manual testing checklist completed 46 | -------------------------------------------------------------------------------- /app/backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "product-catalog-api" 3 | version = "0.1.0" 4 | description = "E-commerce product catalog API - Module 1 Exercise" 5 | requires-python = ">=3.12" 6 | dependencies = [ 7 | "fastapi>=0.118.0", 8 | "httpx>=0.28.1", 9 | "pydantic>=2.11.10", 10 | "pydantic-settings>=2.11.0", 11 | "pytest>=8.4.2", 12 | "ruff>=0.8.4", 13 | "uvicorn>=0.37.0", 14 | ] 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | 20 | [tool.hatch.build.targets.wheel] 21 | packages = ["app"] 22 | 23 | # Ruff configuration for linting and formatting 24 | [tool.ruff] 25 | line-length = 120 26 | target-version = "py312" 27 | 28 | # Enable auto-fixing where possible 29 | fix = true 30 | 31 | # Exclude common directories 32 | exclude = [ 33 | ".venv", 34 | ".git", 35 | "__pycache__", 36 | ".pytest_cache", 37 | "*.egg-info", 38 | ] 39 | 40 | [tool.ruff.lint] 41 | # Enable essential rule sets for clean, maintainable code 42 | select = [ 43 | "E", # pycodestyle errors 44 | "W", # pycodestyle warnings 45 | "F", # pyflakes 46 | "I", # isort (import sorting) 47 | "N", # pep8-naming 48 | "UP", # pyupgrade (modernize Python code) 49 | "B", # flake8-bugbear (find likely bugs) 50 | "C4", # flake8-comprehensions 51 | "SIM", # flake8-simplify 52 | "RUF", # Ruff-specific rules 53 | ] 54 | 55 | # Ignore specific rules that conflict with our patterns 56 | ignore = [ 57 | "E501", # Line too long (handled by formatter) 58 | ] 59 | 60 | [tool.ruff.lint.isort] 61 | # Import sorting configuration 62 | known-first-party = ["app"] 63 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] 64 | 65 | [tool.ruff.format] 66 | # Use double quotes for strings 67 | quote-style = "double" 68 | # Indent with spaces 69 | indent-style = "space" 70 | # Respect magic trailing comma 71 | skip-magic-trailing-comma = false 72 | # Automatically detect line ending style 73 | line-ending = "auto" 74 | -------------------------------------------------------------------------------- /app/frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", 13 | destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 14 | outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 21 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", 22 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 23 | icon: "size-9", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | } 31 | ); 32 | 33 | function Button({ 34 | className, 35 | variant, 36 | size, 37 | asChild = false, 38 | ...props 39 | }: React.ComponentProps<"button"> & 40 | VariantProps & { 41 | asChild?: boolean; 42 | }) { 43 | const Comp = asChild ? Slot : "button"; 44 | 45 | return ; 46 | } 47 | 48 | export { Button, buttonVariants }; 49 | -------------------------------------------------------------------------------- /app/backend/app/api/products.py: -------------------------------------------------------------------------------- 1 | """ 2 | Product API endpoints. 3 | 4 | This module defines all HTTP endpoints related to product operations. 5 | Each endpoint delegates business logic to the service layer. 6 | """ 7 | 8 | from fastapi import APIRouter 9 | 10 | from app.core.logging_config import StructuredLogger 11 | from app.models.product import ProductListResponse 12 | from app.services import product_service 13 | 14 | # Initialize router for product endpoints 15 | router = APIRouter(prefix="/api/products", tags=["products"]) 16 | 17 | # Initialize structured logger 18 | logger = StructuredLogger(__name__) 19 | 20 | 21 | @router.get("", response_model=ProductListResponse) 22 | async def get_products() -> ProductListResponse: 23 | """ 24 | Get all products from the catalog. 25 | 26 | This endpoint returns all products currently available in the catalog. 27 | In the future, this endpoint will support filtering by price, category, 28 | and keyword search (that's what you'll be adding in the exercise!). 29 | 30 | Returns: 31 | ProductListResponse containing list of products and total count 32 | 33 | Example Response: 34 | { 35 | "products": [ 36 | { 37 | "product_id": 1, 38 | "product_name": "Wireless Bluetooth Mouse", 39 | "product_description": "Ergonomic wireless mouse...", 40 | "product_price_usd": "29.99", 41 | "product_category": "electronics", 42 | "product_in_stock": true 43 | }, 44 | ... 45 | ], 46 | "total_count": 30 47 | } 48 | """ 49 | logger.info("api_request_received", endpoint="/api/products", http_method="GET", operation="get_products") 50 | 51 | # Delegate to service layer for business logic 52 | products = product_service.get_all_products() 53 | 54 | logger.info( 55 | "api_response_prepared", endpoint="/api/products", products_count=len(products), operation="get_products" 56 | ) 57 | 58 | return ProductListResponse(products=products, total_count=len(products)) 59 | -------------------------------------------------------------------------------- /app/frontend/src/types/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error types matching backend Pydantic ErrorResponse model EXACTLY. 3 | * 4 | * Backend definition (app/models/error.py): 5 | * ```python 6 | * class ErrorResponse(BaseModel): 7 | * error_code: str = Field(..., min_length=1, max_length=100) 8 | * error_message: str = Field(..., min_length=1, max_length=500) 9 | * error_details: dict[str, Any] | None = Field(default=None) 10 | * timestamp_utc: str = Field(default_factory=lambda: datetime.now(UTC).isoformat()) 11 | * ``` 12 | */ 13 | 14 | /** 15 | * Error response model matching backend ErrorResponse. 16 | * 17 | * All API errors follow this consistent structure. 18 | */ 19 | export interface ErrorResponse { 20 | /** Machine-readable error code (e.g., "invalid_price_range", "product_not_found") */ 21 | error_code: string; 22 | 23 | /** Human-readable error message for end users */ 24 | error_message: string; 25 | 26 | /** Optional additional context for debugging */ 27 | error_details?: Record; 28 | 29 | /** ISO 8601 timestamp when error occurred (UTC) */ 30 | timestamp_utc: string; 31 | } 32 | 33 | /** 34 | * Custom error class for API errors with structured information. 35 | * 36 | * Wraps ErrorResponse from backend with HTTP status code. 37 | * Use this for proper error handling in try-catch blocks. 38 | * 39 | * Example: 40 | * ```typescript 41 | * try { 42 | * await fetchProducts(); 43 | * } catch (error) { 44 | * if (error instanceof ApiError) { 45 | * console.error(error.errorResponse.error_code); 46 | * console.error(error.statusCode); 47 | * } 48 | * } 49 | * ``` 50 | */ 51 | export class ApiError extends Error { 52 | constructor( 53 | /** HTTP status code (e.g., 400, 404, 500) */ 54 | public readonly statusCode: number, 55 | /** Structured error response from backend */ 56 | public readonly errorResponse: ErrorResponse 57 | ) { 58 | super(errorResponse.error_message); 59 | this.name = "ApiError"; 60 | 61 | // Maintains proper stack trace for where error was thrown (V8 engines only) 62 | if (Error.captureStackTrace) { 63 | Error.captureStackTrace(this, ApiError); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/frontend/src/APITester.tsx: -------------------------------------------------------------------------------- 1 | import { type FormEvent, useRef } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | export function APITester() { 8 | const responseInputRef = useRef(null); 9 | 10 | const testEndpoint = async (e: FormEvent) => { 11 | e.preventDefault(); 12 | 13 | try { 14 | const form = e.currentTarget; 15 | const formData = new FormData(form); 16 | const endpoint = formData.get("endpoint") as string; 17 | const url = new URL(endpoint, location.href); 18 | const method = formData.get("method") as string; 19 | const res = await fetch(url, { method }); 20 | 21 | const data = await res.json(); 22 | responseInputRef.current!.value = JSON.stringify(data, null, 2); 23 | } catch (error) { 24 | responseInputRef.current!.value = String(error); 25 | } 26 | }; 27 | 28 | return ( 29 |
30 |
34 | 43 | 44 | 55 | 56 | 59 |
60 | 61 |