FastAPI — The Complete Notebook
Comprehensive guide on FastAPI — The Complete Notebook.
FastAPI
1. Overview#
FastAPI is a modern Python web framework for building APIs, built on top of Starlette (for the web layer) and Pydantic (for data validation). It uses standard Python type hints to automatically validate requests, generate interactive documentation, and provide editor autocompletion.
FastAPI runs on an ASGI server (like
uvicorn), which means it natively supportsasync defendpoints for high-concurrency I/O-bound APIs — seeasync-python.mdfor the underlying concepts.
2. Getting Started#
2.1 Installation#
bashpip install fastapi uvicorn
2.2 Minimal App#
🐍 PythonInteractive WebAssembly# file: main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, World!"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
bashuvicorn main:app --reload
main= the filemain.py,app= the FastAPI instance inside it.--reloadrestarts the server automatically on code changes (development only).- Visit
http://127.0.0.1:8000/docsfor auto-generated interactive Swagger UI docs.
3. Path & Query Parameters#
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI
app = FastAPI()
# Path parameter — part of the URL itself, type-validated automatically
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# Query parameters — appended after "?", e.g. /search?keyword=python&limit=10
@app.get("/search")
def search(keyword: str, limit: int = 10, offset: int = 0):
return {"keyword": keyword, "limit": limit, "offset": offset}
If a request sends /users/abc instead of a number, FastAPI automatically returns a 422 Unprocessable Entity with a clear validation error — no manual checking needed.
4. Request Bodies with Pydantic Models#
Pydantic models define the shape of incoming JSON data and validate it automatically.
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
in_stock: bool = True # default value — optional in the request body
@app.post("/products")
def create_product(product: Product):
return {"received": product, "total_with_tax": product.price * 1.18}
Sending {"name": "Keyboard", "price": 1499.0} automatically validates types, fills in the default in_stock: True, and rejects malformed payloads with a detailed error response.
4.1 Nested Models & Response Models#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class Address(BaseModel):
city: str
pincode: str
class User(BaseModel):
name: str
address: Address
class UserResponse(BaseModel):
name: str
city: str
@app.post("/users", response_model=UserResponse)
def create_user(user: User):
# response_model filters the output — only fields declared in UserResponse are returned,
# even if the function returns more data internally
return {"name": user.name, "city": user.address.city, "internal_secret": "hidden"}
5. Dependency Injection with Depends#
FastAPI's dependency system lets you share reusable logic (DB sessions, auth checks, pagination defaults) across many endpoints.
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI, Depends
app = FastAPI()
def pagination_params(page: int = 1, page_size: int = 20):
return {"page": page, "page_size": page_size}
@app.get("/items")
def list_items(pagination: dict = Depends(pagination_params)):
return {"pagination": pagination, "items": []}
5.1 Class-Based Dependencies#
🐍 PythonInteractive WebAssemblyclass DBSession:
def __init__(self):
self.connection = "connected-to-db"
def get_db():
db = DBSession()
try:
yield db # dependency can also do setup/teardown, like a fixture
finally:
print("Closing DB session")
@app.get("/orders")
def get_orders(db: DBSession = Depends(get_db)):
return {"connection": db.connection}
6. Securing Endpoints with an X-API-Key Header#
6.1 Basic API Key Check#
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI, Security, HTTPException, status
from fastapi.security import APIKeyHeader
API_KEY = "your-secret-key" # in production: load from environment variables / a secrets manager
api_key_header = APIKeyHeader(name="X-API-Key")
app = FastAPI()
def verify_api_key(key: str = Security(api_key_header)) -> str:
if key != API_KEY:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid or missing API key",
)
return key
@app.get("/secure-data", dependencies=[Security(verify_api_key)])
def get_secure_data():
return {"message": "You accessed protected data"}
A client must now send:
›GET /secure-data X-API-Key: your-secret-key
Missing or wrong keys get an automatic 403 Forbidden.
6.2 Applying Security to an Entire Router#
Instead of repeating dependencies=[Security(verify_api_key)] on every endpoint, apply it once to a whole router.
🐍 PythonInteractive WebAssemblyfrom fastapi import APIRouter, Security
router = APIRouter(dependencies=[Security(verify_api_key)])
@router.get("/orders")
def get_orders():
return {"orders": []}
@router.get("/invoices")
def get_invoices():
return {"invoices": []}
app.include_router(router, prefix="/api/v1")
6.3 Storing Multiple Valid Keys (Per-Client)#
🐍 PythonInteractive WebAssemblyimport os
from fastapi import Security, HTTPException, status
from fastapi.security import APIKeyHeader
# In production: pull from a database or secrets manager, not hardcoded values
VALID_API_KEYS = {
"key-abc123": "client_a",
"key-def456": "client_b",
}
api_key_header = APIKeyHeader(name="X-API-Key")
def verify_api_key(key: str = Security(api_key_header)) -> str:
client = VALID_API_KEYS.get(key)
if client is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API key")
return client # downstream endpoints can now know WHICH client made the request
@app.get("/secure-data")
def get_secure_data(client: str = Security(verify_api_key)):
return {"message": f"Hello, {client}"}
6.4 Loading Secrets from Environment Variables#
🐍 PythonInteractive WebAssemblyimport os
from dotenv import load_dotenv
load_dotenv() # loads variables from a .env file into the environment
API_KEY = os.environ["API_KEY"] # raises KeyError loudly if missing — safer than a silent default
Mathematical Formulation# .env (never commit this file — add it to .gitignore) API_KEY=your-secret-key
X-API-Keyis simple and fine for service-to-service or internal API auth, but it's a static, long-lived secret. For user-facing auth with logins, sessions, or scoped permissions, prefer OAuth2 with JWT (FastAPI has built-in support viafastapi.security.OAuth2PasswordBearer).
7. Error Handling#
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI, HTTPException
app = FastAPI()
fake_db = {"1": "Laptop", "2": "Mouse"}
@app.get("/products/{product_id}")
def get_product(product_id: str):
if product_id not in fake_db:
raise HTTPException(status_code=404, detail="Product not found")
return {"product_id": product_id, "name": fake_db[product_id]}
7.1 Custom Exception Handlers#
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class OutOfStockError(Exception):
def __init__(self, product_id: str):
self.product_id = product_id
@app.exception_handler(OutOfStockError)
def out_of_stock_handler(request: Request, exc: OutOfStockError):
return JSONResponse(
status_code=409,
content={"message": f"Product {exc.product_id} is out of stock"},
)
@app.get("/buy/{product_id}")
def buy_product(product_id: str):
raise OutOfStockError(product_id)
8. Async Endpoints#
🐍 PythonInteractive WebAssemblyimport httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/external-data")
async def get_external_data():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()
Use
async defwhen the endpoint awaits I/O (HTTP calls, async DB drivers). Use a regulardefif the logic is synchronous/CPU-light — FastAPI runs those in a thread pool automatically so they don't block the event loop.
9. Project Structure for a Real App#
codemy_api/ ├── main.py # creates the FastAPI app, includes routers ├── routers/ │ ├── users.py │ └── products.py ├── models/ │ └── schemas.py # Pydantic models ├── dependencies/ │ └── auth.py # verify_api_key and other shared dependencies ├── .env └── requirements.txt
🐍 PythonInteractive WebAssembly# main.py
from fastapi import FastAPI
from routers import users, products
app = FastAPI(title="My API", version="1.0.0")
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(products.router, prefix="/products", tags=["products"])
10. Common Pitfalls#
INCORRECT: Hardcoding Secrets in Source Code#
🐍 PythonInteractive WebAssemblyAPI_KEY = "sk-abc123" # committed to git history forever
CORRECT: Fix — Environment Variables + .gitignore#
🐍 PythonInteractive WebAssemblyAPI_KEY = os.environ["API_KEY"]
INCORRECT: Blocking Calls Inside async def Endpoints#
🐍 PythonInteractive WebAssembly@app.get("/data")
async def get_data():
time.sleep(3) # blocks the entire event loop for every request being served
CORRECT: Fix#
🐍 PythonInteractive WebAssembly@app.get("/data")
def get_data(): # plain def — FastAPI runs it in a thread pool automatically
time.sleep(3)
INCORRECT: Comparing API Keys with == in Security-Critical Code#
Simple == comparison is vulnerable to timing attacks in theory — for high-security systems, use a constant-time comparison.
🐍 PythonInteractive WebAssemblyimport hmac
def verify_api_key(key: str = Security(api_key_header)) -> str:
if not hmac.compare_digest(key, API_KEY):
raise HTTPException(status_code=403, detail="Invalid API key")
return key
11. Summary & Best Practices Checklist#
- Use Pydantic models for all request bodies — never parse raw JSON manually.
- Use
Depends/Securityfor shared logic (auth, pagination, DB sessions) instead of repeating code per endpoint. - Never hardcode API keys or secrets — load them from environment variables.
- Apply security dependencies at the router level to protect groups of endpoints at once.
- Use
async defonly for endpoints that actuallyawaitI/O; keep CPU-bound logic in regulardefendpoints. - Return a
response_modelto control exactly what data leaves the API. - Use
HTTPExceptionfor expected errors and custom exception handlers for domain-specific ones. - For real security beyond simple service auth, move to OAuth2/JWT instead of a static API key.
FastAPI & Dependency Injection Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.