404 Not Found

404 Not Found


nginx

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


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.

PYTHON
# 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

100%
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

PYTHON
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:

TEXT
# Function defined successfully

4. Request-Response Lifecycle

(1) Complete Workflow

100%
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

PYTHON
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:

TEXT
# Function defined successfully

(2) ▶ Example: Updating and Deleting Endpoints

PYTHON
@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:

TEXT
# 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

PYTHON
@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:

TEXT
# 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

PYTHON
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:

TEXT
# Function defined successfully

❓ FAQ

Q What should I do if data is lost from in-memory storage after a restart?
A Phase 1 uses in-memory storage to simplify learning; in Phase 2, it will be replaced with PostgreSQL + SQLAlchemy. This is a progressive learning strategy.
Q Why are ProductCreate and ProductResponse separated?
A Separation of concerns: The 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.
Q What is the purpose of exclude_unset=True in the PUT endpoint?
A It allows the client to update only the fields it wants to change; fields not provided are not overwritten and remain set to None. Combined with the optional fields in ProductUpdate, this enables partial updates.
Q How can I test whether an OpenAPI document can be consumed by a frontend application?
A Visit /openapi.json, use npx openapi-typescript to generate TypeScript types, or use Swagger Codegen to generate an SDK.
Q How are the values of enum parameters displayed in the documentation?
A FastAPI automatically displays all enum values in the Swagger UI drop-down menu, making them immediately clear to Bob, the front-end developer.
Q How can duplication be avoided when multiple endpoints share a model?
A Use model inheritance: Define common fields in ProductBase(BaseModel), and have ProductCreate(ProductBase) and ProductResponse(ProductBase) inherit and extend them.

📖 Summary


📝 Exercises

  1. 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
  2. Advanced Exercise (Difficulty ⭐⭐): Add the /products/{id}/prices endpoint to query all price records for a specified product, supporting the sort (date/price) and limit query parameters. Hint: product_id path parameter + sort/limit query parameters
  3. Challenge (Difficulty: ⭐⭐⭐): Implement a dual-view system using ProductAdmin and ProductPublic—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: Two response_model instances + the same data source

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏