Intermediate
15 min read
#Python#FastAPI#REST API#Pydantic#Security

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 supports async def endpoints for high-concurrency I/O-bound APIs — see async-python.md for the underlying concepts.


2. Getting Started#

2.1 Installation#

bash
pip install fastapi uvicorn

2.2 Minimal App#

🐍 Python
# 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}
bash
uvicorn main:app --reload
  • main = the file main.py, app = the FastAPI instance inside it.
  • --reload restarts the server automatically on code changes (development only).
  • Visit http://127.0.0.1:8000/docs for auto-generated interactive Swagger UI docs.

3. Path & Query Parameters#

🐍 Python
from 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.

🐍 Python
from 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#

🐍 Python
from 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.

🐍 Python
from 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#

🐍 Python
class 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#

🐍 Python
from 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.

🐍 Python
from 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)#

🐍 Python
import 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#

🐍 Python
import 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-Key is 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 via fastapi.security.OAuth2PasswordBearer).


7. Error Handling#

🐍 Python
from 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#

🐍 Python
from 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#

🐍 Python
import 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 def when the endpoint awaits I/O (HTTP calls, async DB drivers). Use a regular def if 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#

code
my_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
🐍 Python
# 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#

🐍 Python
API_KEY = "sk-abc123" # committed to git history forever

CORRECT: Fix — Environment Variables + .gitignore#

🐍 Python
API_KEY = os.environ["API_KEY"]

INCORRECT: Blocking Calls Inside async def Endpoints#

🐍 Python
@app.get("/data") async def get_data(): time.sleep(3) # blocks the entire event loop for every request being served

CORRECT: Fix#

🐍 Python
@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.

🐍 Python
import 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/Security for 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 def only for endpoints that actually await I/O; keep CPU-bound logic in regular def endpoints.
  • Return a response_model to control exactly what data leaves the API.
  • Use HTTPException for 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.
Knowledge Checkpoint

FastAPI & Dependency Injection Checkpoint

Q1.What function in FastAPI is used to declare dependency injection for database sessions, authentication, or query parameters?
ADepends()
BInject()
CProvide()
DMiddleware()
Q2.How should long-running CPU-bound calculations (e.g. heavy image processing) be handled in a FastAPI route without blocking other concurrent requests?
ADefine the route with `async def` and run the computation synchronously.
BRun the heavy CPU task in a separate process/worker pool or background task worker (e.g. Celery / `ProcessPoolExecutor`), or define regular `def` so FastAPI offloads it to a threadpool.
CIncrease the `uvicorn` timeout to 3600 seconds.
DCall `time.sleep()` in the route.
Q3.Which ASGI server is standard for deploying production FastAPI applications?
AWSGI Gunicorn standalone
BUvicorn (or Gunicorn with Uvicorn worker class)
CApache HTTP Server mod_python
DNginx unit pure
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.