Phase 1 Comprehensive Exercise — Building the PriceTracker Basic API
The concepts covered in the five lessons of Phase 1 are like five puzzle pieces. Now it's time to put them together to form a complete picture—the PriceTracker Basic API—so that Bob's front end can actually consume the data.
1. What You'll Learn
- Design and implement three sets of core endpoints:
/products,/products/{id}, and/prices - The complete Pydantic V2 model system:
ProductCreate/ProductResponse/PriceCreate/PriceResponse - Practical Examples of Combining Path Parameters, Query Parameters, and Request Bodies
- Use
response_modelto implement the business logic for "publicly querying hidden wholesale prices" - Bob (Front-end Integration): Confirm that the OpenAPI documentation can be automatically generated by the front end for use by the SDK
2. Alice's True Story
(1) Pain Point: Fragmented knowledge cannot be assembled into a product
Alice has finished the first five lessons, but the examples in each lesson are standalone snippets—the examples on path parameters and Pydantic aren't connected. Bob couldn't wait any longer: "I need a working API, not a bunch of scattered demos!" Alice needs to integrate routing design, parameter validation, data validation, and response filtering into a single, functional API service.
(2) Comprehensive Solutions for Real-World Problems
This lesson integrates all the concepts covered in the first five lessons into the PriceTracker Basic API, which includes a complete set of CRUD endpoints, a Pydantic model system, a parameter validation chain, and response filtering logic.
# Complete PriceTracker basic API structure
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="PriceTracker API", version="0.1.0")
# Models, routes, validation — all integrated
(3) Revenue
Alice has a working API service, and Bob can use Swagger UI to test all endpoints; the automatically generated OpenAPI documentation allows front-end SDKs to consume the API directly.
3. Comprehensive Design of API Endpoints
(1) Phase 1 Endpoint Planning
flowchart LR
Client[Client] -->|GET /products| List[List Products]
Client -->|POST /products| Create[Create Product]
Client -->|GET /products/id| Detail[Product Detail]
Client -->|PUT /products/id| Update[Update Product]
Client -->|DELETE /products/id| Delete[Delete Product]
Client -->|GET /prices| Search[Search Prices]
Client -->|POST /prices| AddPrice[Add Price]
List --> PM[ProductResponse]
Create --> PM
Detail --> PDP[ProductDetailResponse]
Search --> PRM[PriceResponse]
AddPrice --> PRM
| Endpoint | Method | Path Parameters | Query Parameters | Request Body | Response Model |
|---|---|---|---|---|---|
| Product List | GET | - | category, sort, limit, offset | - | list[ProductResponse] |
| Create Product | POST | - | - | ProductCreate |
ProductResponse |
| Product Details | GET | product_id | - | - | ProductDetailResponse |
| Update Product | PUT | product_id | - | ProductUpdate |
ProductResponse |
| Delete Product | DELETE | product_id | - | - | dict |
| Price Inquiry | GET | - | product_id, min_price, max_price | - | list[PriceResponse] |
| Add Price | POST | - | - | PriceCreate |
PriceResponse |
(1) ▶ Example: Complete Pydantic Model System
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional
from enum import Enum
from datetime import datetime
class Category(str, Enum):
electronics = "electronics"
clothing = "clothing"
food = "food"
books = "books"
class PriceInfo(BaseModel):
amount: float = Field(gt=0, description="Price amount in USD")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: Category
base_price: float = Field(gt=0, description="Base price in USD")
description: Optional[str] = Field(None, max_length=2000)
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
category: Optional[Category] = None
base_price: Optional[float] = Field(None, gt=0)
description: Optional[str] = Field(None, max_length=2000)
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
category: str
base_price: float
class ProductDetailResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
category: str
base_price: float
description: Optional[str] = None
created_at: datetime
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0, description="Price in USD")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
source: str = Field(max_length=100)
@field_validator("price")
@classmethod
def round_price(cls, v: float) -> float:
return round(v, 2)
class PriceResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
product_id: int
price: float
currency: str
source: str
recorded_at: datetime
Output:
# Function defined successfully
4. Request-Response Lifecycle
(1) Complete Workflow
sequenceDiagram
participant Client as Bob Frontend
participant FastAPI as FastAPI Router
participant Pydantic as Pydantic Validator
participant Handler as View Function
participant DB as In-Memory DB
Client->>FastAPI: POST /products (JSON body)
FastAPI->>Pydantic: Validate with ProductCreate
Pydantic-->>FastAPI: Validated model instance
FastAPI->>Handler: create_product(data: ProductCreate)
Handler->>DB: Store product
DB-->>Handler: Stored record
Handler-->>FastAPI: Return full dict
FastAPI->>Pydantic: Filter with ProductResponse
Pydantic-->>Client: JSON response (filtered)
(1) ▶ Example: Implementing Product CRUD Endpoints
from fastapi import FastAPI, Path, Query, HTTPException
from datetime import datetime
app = FastAPI(title="PriceTracker API", version="0.1.0")
# In-memory storage (will be replaced with DB in Phase 2)
products_db: dict[int, dict] = {}
prices_db: list[dict] = []
_product_counter = 0
_price_counter = 0
@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
global _product_counter
_product_counter += 1
record = {
"id": _product_counter,
"name": product.name,
"category": product.category.value,
"base_price": product.base_price,
"description": product.description,
"created_at": datetime.utcnow(),
}
products_db[_product_counter] = record
return record
@app.get("/products", response_model=list[ProductResponse])
async def list_products(
category: Optional[Category] = Query(None),
sort: str = Query("name", pattern="^(name|base_price)$"),
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
):
items = list(products_db.values())
if category:
items = [p for p in items if p["category"] == category.value]
items.sort(key=lambda p: p.get(sort, ""))
return items[offset : offset + limit]
@app.get("/products/{product_id}", response_model=ProductDetailResponse)
async def get_product(
product_id: int = Path(gt=0, description="Product ID"),
):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
return products_db[product_id]
Output:
# Function defined successfully
(2) ▶ Example: Updating and Deleting Endpoints
@app.put("/products/{product_id}", response_model=ProductResponse)
async def update_product(
product_id: int = Path(gt=0),
update: ProductUpdate = ...,
):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
record = products_db[product_id]
update_data = update.model_dump(exclude_unset=True)
record.update(update_data)
return record
@app.delete("/products/{product_id}")
async def delete_product(product_id: int = Path(gt=0)):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
del products_db[product_id]
return {"message": "Product deleted"}
Output:
# Function defined successfully
5. Price Endpoints and Portfolio Strategies in Practice
(1) Combination of Query Parameters and Request Body
(1) ▶ Example: Price Query and Create Endpoints
@app.get("/prices", response_model=list[PriceResponse])
async def search_prices(
product_id: Optional[int] = Query(None, gt=0),
min_price: float = Query(0, ge=0, description="Min price in USD"),
max_price: float = Query(999999, ge=0, description="Max price in USD"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
):
results = prices_db
if product_id:
results = [p for p in results if p["product_id"] == product_id]
results = [p for p in results if min_price <= p["price"] <= max_price]
return results[offset : offset + limit]
@app.post("/prices", response_model=PriceResponse, status_code=201)
async def create_price(price: PriceCreate):
global _price_counter
if price.product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
_price_counter += 1
record = {
"id": _price_counter,
"product_id": price.product_id,
"price": price.price,
"currency": price.currency,
"source": price.source,
"recorded_at": datetime.utcnow(),
}
prices_db.append(record)
return record
Output:
# Function defined successfully
6. Practical Guide to Response Filtering: Public Version vs. Admin Version
(1) Business Scenario: Hiding Wholesale Prices
Retail users of PriceTracker can only view retail prices, while administrators can view wholesale and cost prices.
(1) ▶ Example: Multi-Role Response Model
class ProductPublicResponse(BaseModel):
"""Public view - hide wholesale and cost prices"""
id: int
name: str
category: str
retail_price: float
class ProductAdminResponse(BaseModel):
"""Admin view - show full price chain"""
id: int
name: str
category: str
retail_price: float
wholesale_price: float
cost_price: float
margin_pct: float # Profit margin percentage
# Extended in-memory storage with pricing tiers
admin_products_db: dict[int, dict] = {}
@app.get("/products/{product_id}/public", response_model=ProductPublicResponse)
async def get_product_public(product_id: int = Path(gt=0)):
if product_id not in admin_products_db:
raise HTTPException(status_code=404, detail="Product not found")
return admin_products_db[product_id]
@app.get("/products/{product_id}/admin", response_model=ProductAdminResponse)
async def get_product_admin(product_id: int = Path(gt=0)):
if product_id not in admin_products_db:
raise HTTPException(status_code=404, detail="Product not found")
return admin_products_db[product_id]
Output:
# Function defined successfully
❓ FAQ
ProductCreate and ProductResponse separated?Create model does not include an ID (which is generated on the server), while the Response model includes an ID. This is also a security best practice—the client should not specify the ID.exclude_unset=True in the PUT endpoint?None. Combined with the optional fields in ProductUpdate, this enables partial updates./openapi.json, use npx openapi-typescript to generate TypeScript types, or use Swagger Codegen to generate an SDK.ProductBase(BaseModel), and have ProductCreate(ProductBase) and ProductResponse(ProductBase) inherit and extend them.📖 Summary
- Phase 1: Completed three sets of core endpoints for PriceTracker: product CRUD, price query/creation, and view publication/management
- Pydantic model architecture: Three distinct layers—Create (input validation), Update (partial updates), and Response (output filtering)
- Path parameters, query parameters, and the request body are naturally combined; FastAPI automatically distinguishes them by type.
response_modelAchieving "Same Data, Different Views"—The Public Version Hides Wholesale Prices, While the Admin Version Displays the Full Price Chain- Automatic OpenAPI documentation generation; front-end developers can use Swagger UI to test or generate SDKs directly
📝 Exercises
- Basic Exercise (Difficulty ⭐): Combine all the code from this lesson into a single
app/main.py, start the service, and use Swagger UI to create three products and query the list. Hint:uvicorn app.main:app --reload - Advanced Exercise (Difficulty ⭐⭐): Add the
/products/{id}/pricesendpoint to query all price records for a specified product, supporting thesort(date/price) andlimitquery parameters. Hint:product_idpath parameter +sort/limitquery parameters - Challenge (Difficulty: ⭐⭐⭐): Implement a dual-view system using
ProductAdminandProductPublic—create product data containing the complete price chain. The public endpoint should return only the retail price, while the administrative endpoint should return the retail price, wholesale price, and profit margin. Hint: Tworesponse_modelinstances + the same data source
---|



