Python FastAPI Practice Problems & Exercises
Practice: FastAPI
← Back to lessonCreate a basic FastAPI GET route and verify it with TestClient.Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
# TODO: Add GET /ping that returns {"message": "pong", "version": "1.0"}
client = TestClient(app)
resp = client.get('/ping')
print(f"Status: {resp.status_code}")
print(f"JSON: {resp.json()}")
Expected Output
Status: 200
JSON: {'message': 'pong', 'version': '1.0'}Hints
Hint 1: Decorate your function with @app.get('/ping').
Hint 2: FastAPI automatically serializes dict return values to JSON.
Hint 3: TestClient from fastapi.testclient wraps the app for synchronous testing.
Define a FastAPI route with both path parameters and optional query parameters using type hints.Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
from typing import Optional
app = FastAPI()
# TODO: GET /items/{item_id}
# Path param: item_id (int)
# Query params: category (str, optional, default None), in_stock (bool, default True)
# Return all three values in a dict
client = TestClient(app)
r1 = client.get('/items/42')
print(r1.json())
r2 = client.get('/items/7?category=electronics&in_stock=false')
print(r2.json())
Expected Output
{'item_id': 42, 'category': None, 'in_stock': True}
{'item_id': 7, 'category': 'electronics', 'in_stock': False}Hints
Hint 1: Declare path params in the route string and as function arguments with matching names.
Hint 2: Query params are any function arguments NOT in the path — FastAPI infers them automatically.
Hint 3: Optional[str] = None makes a query param optional; bool params accept 'true'/'false'.
Define a Pydantic model for a POST request body and use it in a FastAPI route.Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# TODO: Define a Pydantic model CreateUserRequest with:
# - name: str
# - email: str
# - age: Optional[int] = None
# POST /users accepts this body and returns:
# {"id": 1, "name": ..., "email": ..., "age": ...}
client = TestClient(app)
r1 = client.post('/users', json={'name': 'Alice', 'email': '[email protected]', 'age': 30})
print(f"{r1.status_code}: {r1.json()}")
r2 = client.post('/users', json={'name': 'Bob', 'email': '[email protected]'})
print(f"{r2.status_code}: {r2.json()}")
Expected Output
201: {'id': 1, 'name': 'Alice', 'email': '[email protected]', 'age': 30}
201: {'id': 2, 'name': 'Bob', 'email': '[email protected]', 'age': None}Hints
Hint 1: Define a class that inherits from BaseModel with typed fields.
Hint 2: Use the model as a type hint for the request body parameter in your route function.
Hint 3: FastAPI automatically parses and validates the JSON body into your model instance.
Use response_model to automatically strip sensitive fields (like password hash) from API responses.Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Full internal user model (includes password hash)
class UserDB(BaseModel):
id: int
name: str
email: str
password_hash: str
role: str
# TODO: Define UserPublic (id, name, email, role — no password_hash)
# GET /users/{uid} uses response_model=UserPublic so password_hash is stripped
USERS_DB = {
1: UserDB(id=1, name='Alice', email='[email protected]', password_hash='$2b$hashed', role='admin'),
}
client = TestClient(app)
resp = client.get('/users/1')
data = resp.json()
print(f"has password_hash: {'password_hash' in data}")
print(f"fields: {sorted(data.keys())}")
Expected Output
has password_hash: False
fields: ['email', 'id', 'name', 'role']Hints
Hint 1: Define UserPublic as a Pydantic model without the password_hash field.
Hint 2: Pass response_model=UserPublic to the @app.get() decorator.
Hint 3: FastAPI will serialize the returned object through the response_model, stripping extra fields.
Raise HTTPException with meaningful detail messages for product-not-found and insufficient-stock scenarios.Solution
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
PRODUCTS = {
1: {'id': 1, 'name': 'Widget', 'stock': 10},
2: {'id': 2, 'name': 'Gadget', 'stock': 0},
}
# TODO: POST /products/{product_id}/purchase
# Path param: product_id (int)
# Body: {"quantity": int}
# Raise 404 if product not found: detail="Product {id} not found"
# Raise 409 if insufficient stock: detail="Only {stock} units available"
# On success return {"purchased": product_name, "remaining_stock": new_stock}
client = TestClient(app)
r1 = client.post('/products/1/purchase', json={'quantity': 3})
print(f"{r1.status_code}: {r1.json()}")
r2 = client.post('/products/2/purchase', json={'quantity': 1})
print(f"{r2.status_code}: {r2.json()}")
r3 = client.post('/products/99/purchase', json={'quantity': 1})
print(f"{r3.status_code}: {r3.json()}")
Expected Output
200: {'purchased': 'Widget', 'remaining_stock': 7}
409: {'detail': 'Only 0 units available'}
404: {'detail': 'Product 99 not found'}Hints
Hint 1: raise HTTPException(status_code=404, detail='...') raises an HTTP error response.
Hint 2: FastAPI wraps the detail string in {'detail': '...'} automatically.
Hint 3: Check for the product first, then check stock — order matters for correct error messages.
Create a reusable pagination dependency with Depends and inject it into a route.Solution
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
app = FastAPI()
# TODO: Create a pagination dependency function get_pagination(skip: int = 0, limit: int = 10)
# that returns {"skip": skip, "limit": min(limit, 100)} (cap limit at 100)
# Use it with Depends() in a GET /items route that returns
# {"items": list(range(skip, skip + limit)), "pagination": pagination_dict}
client = TestClient(app)
r1 = client.get('/items')
print(r1.json())
r2 = client.get('/items?skip=10&limit=5')
print(r2.json())
r3 = client.get('/items?limit=500') # should cap at 100
print(f"limit capped: {r3.json()['pagination']['limit']}")
Expected Output
{'items': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'pagination': {'skip': 0, 'limit': 10}}
{'items': [10, 11, 12, 13, 14], 'pagination': {'skip': 10, 'limit': 5}}
limit capped: 100Hints
Hint 1: Define a regular function get_pagination(skip: int = 0, limit: int = 10) that returns a dict.
Hint 2: In your route function, add pagination: dict = Depends(get_pagination).
Hint 3: FastAPI resolves the dependency automatically, injecting the return value.
Write an async FastAPI route that awaits a simulated database call.Solution
import asyncio
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
# Simulated async database call
async def fetch_user_from_db(user_id: int):
await asyncio.sleep(0) # simulate I/O
if user_id == 1:
return {'id': 1, 'name': 'Alice', 'email': '[email protected]'}
return None
# TODO: Add async GET /users/{user_id} that:
# - awaits fetch_user_from_db
# - returns the user or {"error": "not found"} with 404
client = TestClient(app)
r1 = client.get('/users/1')
print(f"{r1.status_code}: {r1.json()}")
r2 = client.get('/users/99')
print(f"{r2.status_code}: {r2.json()}")
Expected Output
200: {'id': 1, 'name': 'Alice', 'email': '[email protected]'}
404: {'error': 'not found'}Hints
Hint 1: Use async def for the route function to make it async.
Hint 2: Use await inside the function to call the async database function.
Hint 3: FastAPI's TestClient handles async routes transparently.
Build a two-level dependency chain: token extraction then user lookup, both injected via Depends.Solution
from fastapi import FastAPI, Depends, HTTPException, Header
from fastapi.testclient import TestClient
from typing import Optional
app = FastAPI()
# Simulated DB
FAKE_DB = {
'user-1': {'id': 1, 'name': 'Alice', 'role': 'admin'},
'user-2': {'id': 2, 'name': 'Bob', 'role': 'user'},
}
# TODO: Implement a two-level dependency chain:
# 1. get_token(authorization: str = Header(...)) -> str
# Extract Bearer token from Authorization header; raise 401 if missing/invalid format
# 2. get_current_user(token: str = Depends(get_token)) -> dict
# Look up token in FAKE_DB; raise 401 if not found
# 3. GET /profile uses Depends(get_current_user) to return {"profile": user}
client = TestClient(app)
r1 = client.get('/profile', headers={'Authorization': 'Bearer user-1'})
print(f"{r1.status_code}: {r1.json()}")
r2 = client.get('/profile', headers={'Authorization': 'Bearer unknown'})
print(f"{r2.status_code}: {r2.json()['detail']}")
r3 = client.get('/profile')
print(f"{r3.status_code}")
Expected Output
200: {'profile': {'id': 1, 'name': 'Alice', 'role': 'admin'}}
401: Invalid or expired token
422Hints
Hint 1: Header(...) (with ... as default) makes the header required — FastAPI raises 422 if missing.
Hint 2: Chain dependencies: get_current_user depends on get_token using Depends(get_token).
Hint 3: Dependency functions can themselves have dependencies — FastAPI resolves the full chain.
Use FastAPI's BackgroundTasks to send a welcome email after returning the signup response immediately.Solution
import time
from fastapi import FastAPI, BackgroundTasks
from fastapi.testclient import TestClient
app = FastAPI()
task_log = []
def send_welcome_email(email: str, username: str):
"""Simulated slow email sending (background task)."""
time.sleep(0.01)
task_log.append(f"email sent to {email} for {username}")
# TODO: POST /users/signup
# Body: {"username": str, "email": str}
# Register the background task (send_welcome_email)
# Return {"message": "signed up", "username": ...} IMMEDIATELY
# The email should be sent in the background after the response is returned
from pydantic import BaseModel
client = TestClient(app)
start = time.time()
resp = client.post('/users/signup', json={'username': 'alice', 'email': '[email protected]'})
elapsed = time.time() - start
print(f"{resp.status_code}: {resp.json()}")
print(f"Response returned quickly: {elapsed < 0.05}")
print(f"Background task ran: {len(task_log) > 0}")
print(f"Task: {task_log[0] if task_log else 'none'}")
Expected Output
201: {'message': 'signed up', 'username': 'alice'}
Response returned quickly: True
Background task ran: True
Task: email sent to [email protected] for aliceHints
Hint 1: Add background_tasks: BackgroundTasks as a parameter to your route function.
Hint 2: Call background_tasks.add_task(send_welcome_email, email, username) to queue the task.
Hint 3: Return the response immediately — FastAPI runs the background task after sending the response.
Define nested Pydantic models with field constraints and compute a derived total in the response.Solution
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
app = FastAPI()
# TODO: Define these Pydantic models:
# Address: street (str), city (str), country (str, 2-char ISO code)
# OrderItem: product_id (int), quantity (int, min=1), unit_price (float, gt=0)
# CreateOrderRequest:
# - customer_name: str (min_length=1)
# - items: List[OrderItem] (min 1 item)
# - shipping_address: Address
# - discount_pct: float (default 0.0, between 0 and 100)
# POST /orders returns the order with a computed total field
# total = sum(item.quantity * item.unit_price for item in items) * (1 - discount_pct/100)
client = TestClient(app)
valid_order = {
'customer_name': 'Alice',
'items': [
{'product_id': 1, 'quantity': 2, 'unit_price': 19.99},
{'product_id': 2, 'quantity': 1, 'unit_price': 49.99},
],
'shipping_address': {'street': '123 Main St', 'city': 'Anytown', 'country': 'US'},
'discount_pct': 10.0,
}
r1 = client.post('/orders', json=valid_order)
print(f"{r1.status_code}: total={r1.json().get('total')}")
invalid_order = dict(valid_order)
invalid_order['items'] = []
r2 = client.post('/orders', json=invalid_order)
print(f"empty items: {r2.status_code}")
Expected Output
201: total=80.982
empty items: 422Hints
Hint 1: Use Field(min_length=1) for string constraints and Field(ge=1) for int minimum.
Hint 2: Field(min_items=1) or annotated List validation can enforce minimum list length in Pydantic v2.
Hint 3: Compute the total in the route handler after receiving the validated model.
Implement a full CRUD FastAPI app with lifespan startup seeding, verified by integration tests.Solution
from fastapi import FastAPI, HTTPException, Depends
from fastapi.testclient import TestClient
from pydantic import BaseModel
from typing import Dict, Optional
from contextlib import asynccontextmanager
# Simulated database
db: Dict[int, dict] = {}
_id_counter = 0
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup: seed data
global _id_counter
db[1] = {'id': 1, 'title': 'Seeded Task', 'done': False}
_id_counter = 1
yield
# shutdown: clear
db.clear()
app = FastAPI(lifespan=lifespan)
class TaskCreate(BaseModel):
title: str
done: bool = False
# TODO: Implement CRUD routes using the db dict above:
# GET /tasks -> list all tasks
# POST /tasks -> create task (201)
# GET /tasks/{tid} -> get task (404 if missing)
# PATCH /tasks/{tid} -> update done status: body {"done": bool} (404 if missing)
# DELETE /tasks/{tid} -> delete (204 or 404)
with TestClient(app) as client:
# List (includes seeded task)
r = client.get('/tasks')
print(f"initial: {[t['title'] for t in r.json()]}")
# Create
r = client.post('/tasks', json={'title': 'New Task'})
tid = r.json()['id']
print(f"created: id={tid}, status={r.status_code}")
# Update
r = client.patch(f'/tasks/{tid}', json={'done': True})
print(f"updated: done={r.json()['done']}")
# Delete
r = client.delete(f'/tasks/{tid}')
print(f"deleted: {r.status_code}")
# 404 after delete
r = client.get(f'/tasks/{tid}')
print(f"after delete: {r.status_code}")
Expected Output
initial: ['Seeded Task']
created: id=2, status=201
updated: done=True
deleted: 204
after delete: 404Hints
Hint 1: Use the module-level `db` dict and `_id_counter` as your in-memory store.
Hint 2: The lifespan context manager runs startup code before yield and shutdown after.
Hint 3: For PATCH, only update the 'done' field — merge with the existing task dict.
