Lab 09: REST APIs — FastAPI
Objective
Time
Prerequisites
Tools
Lab Instructions
Step 1: FastAPI Basics with Pydantic Models
docker run --rm zchencow/innozverse-python:latest python3 -c "
from fastapi import FastAPI, HTTPException, Query, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, field_validator
from typing import Optional
from datetime import datetime
# Pydantic models
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0, description='Price in USD')
stock: int = Field(default=0, ge=0)
category: str = Field(default='General')
class ProductCreate(ProductBase):
pass
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1)
price: Optional[float] = Field(None, gt=0)
stock: Optional[int] = Field(None, ge=0)
category: Optional[str] = None
class ProductResponse(ProductBase):
id: int
status: str
created_at: datetime
class Config:
from_attributes = True
# In-memory store
class Store:
def __init__(self):
self.products: dict[int, dict] = {}
self.next_id = 1
def create(self, data: dict) -> dict:
p = {**data, 'id': self.next_id, 'created_at': datetime.now()}
p['status'] = 'out_of_stock' if p['stock'] == 0 else 'active'
self.products[self.next_id] = p
self.next_id += 1
return p
def get(self, pid: int) -> dict | None:
return self.products.get(pid)
def list(self, category: str = None, min_stock: int = None) -> list[dict]:
items = list(self.products.values())
if category: items = [p for p in items if p['category'] == category]
if min_stock is not None: items = [p for p in items if p['stock'] >= min_stock]
return items
def update(self, pid: int, data: dict) -> dict | None:
p = self.products.get(pid)
if not p: return None
p.update({k: v for k, v in data.items() if v is not None})
p['status'] = 'out_of_stock' if p['stock'] == 0 else 'active'
return p
def delete(self, pid: int) -> bool:
return self.products.pop(pid, None) is not None
store = Store()
app = FastAPI(title='innoZverse Product API', version='1.0.0')
@app.get('/products', response_model=list[dict], tags=['Products'])
def list_products(
category: Optional[str] = Query(None, description='Filter by category'),
min_stock: Optional[int] = Query(None, ge=0, description='Min stock'),
):
return store.list(category=category, min_stock=min_stock)
@app.post('/products', status_code=201, tags=['Products'])
def create_product(product: ProductCreate):
return store.create(product.model_dump())
@app.get('/products/{product_id}', tags=['Products'])
def get_product(product_id: int = Path(..., gt=0)):
p = store.get(product_id)
if not p: raise HTTPException(status_code=404, detail=f'Product {product_id} not found')
return p
@app.patch('/products/{product_id}', tags=['Products'])
def update_product(product_id: int, update: ProductUpdate):
p = store.update(product_id, update.model_dump(exclude_unset=True))
if not p: raise HTTPException(status_code=404, detail='Product not found')
return p
@app.delete('/products/{product_id}', status_code=204, tags=['Products'])
def delete_product(product_id: int):
if not store.delete(product_id):
raise HTTPException(status_code=404, detail='Product not found')
# Test with TestClient (no server needed)
client = TestClient(app)
# Create
r = client.post('/products', json={'name': 'Surface Pro 12\"', 'price': 864.0, 'stock': 15, 'category': 'Laptop'})
print(f'POST /products: {r.status_code} → id={r.json()[\"id\"]}')
client.post('/products', json={'name': 'Surface Pen', 'price': 49.99, 'stock': 80, 'category': 'Accessory'})
client.post('/products', json={'name': 'Office 365', 'price': 99.99, 'stock': 999, 'category': 'Software'})
# List
r = client.get('/products')
print(f'GET /products: {r.status_code} → {len(r.json())} items')
# Get by ID
r = client.get('/products/1')
p = r.json()
print(f'GET /products/1: {r.status_code} → {p[\"name\"]} \${p[\"price\"]}')
# 404
r = client.get('/products/99')
print(f'GET /products/99: {r.status_code} → {r.json()[\"detail\"]}')
# Update
r = client.patch('/products/1', json={'price': 799.99})
print(f'PATCH /products/1: {r.status_code} → price=\${r.json()[\"price\"]}')
# Filter
r = client.get('/products?category=Laptop')
print(f'GET /products?category=Laptop: {len(r.json())} items')
# Validation error
r = client.post('/products', json={'name': '', 'price': -1})
print(f'POST invalid: {r.status_code}')
"Step 2: Dependency Injection & Middleware
Steps 3–8: Background Tasks, Exception Handlers, Response Models, Routers, Streaming, Capstone
Summary
Feature
FastAPI
Notes
Further Reading
Last updated
