Back to Notes

FastAPI

FastAPI

Modern async Python web framework. Standard choice for AI/ML APIs and backend services. Built on Starlette (ASGI) + Pydantic. Auto-generates OpenAPI docs.

pip install fastapi uvicorn[standard]
uvicorn main:app --reload   # dev server
uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4  # production

Minimal App

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="My API", version="1.0.0")

class User(BaseModel):
    name: str
    email: str

@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
    return User(name="Alice", email="alice@example.com")

@app.post("/users", status_code=201)
async def create_user(user: User) -> User:
    # user is already validated Pydantic model
    return user

Visit /docs → interactive Swagger UI. Visit /redoc → ReDoc.


Path, Query, Body Parameters

from fastapi import Query, Path, Body
from typing import Optional

# Path parameter — required
@app.get("/items/{item_id}")
async def get_item(item_id: int = Path(gt=0)):  # validation: > 0
    ...

# Query parameters — optional with default
@app.get("/items")
async def list_items(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, le=100),
    search: Optional[str] = None
):
    ...

# Request body — Pydantic model
class CreateItem(BaseModel):
    name: str
    price: float
    tags: list[str] = []

@app.post("/items")
async def create_item(item: CreateItem):
    ...

# Multiple body params
@app.put("/items/{item_id}")
async def update_item(
    item_id: int,
    item: CreateItem,
    user: User = Body(embed=True)   # nest under "user" key in JSON
):
    ...

Response Models & Status Codes

from fastapi import HTTPException
from fastapi.responses import JSONResponse

class UserResponse(BaseModel):
    id: int
    name: str
    # no password field — response model strips it

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    user = await db.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user  # FastAPI validates against UserResponse automatically

# Custom response
@app.get("/users")
async def list_users() -> list[UserResponse]:
    users = await db.list_users()
    return users

Dependency Injection

FastAPI's DI system is one of its killer features. Declare dependencies as function parameters.

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

# DB session dependency
async def get_db() -> AsyncSession:
    async with SessionLocal() as session:
        yield session            # cleanup runs after request

# Auth dependency
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
) -> User:
    user = await verify_token(token, db)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

# Use in route
@app.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
    return current_user

Dependency chains: get_current_userget_db automatically.


Middleware

from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import time

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Custom middleware
class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        start = time.time()
        response = await call_next(request)
        duration = time.time() - start
        response.headers["X-Process-Time"] = str(duration)
        return response

app.add_middleware(TimingMiddleware)

Background Tasks

from fastapi import BackgroundTasks

def send_email(email: str, message: str):
    # runs after response is sent
    email_client.send(email, message)

@app.post("/register")
async def register(user: CreateUser, background_tasks: BackgroundTasks):
    created = await db.create_user(user)
    background_tasks.add_task(send_email, user.email, "Welcome!")
    return created  # response sent immediately, email sends after

Routers — Modular Structure

# routers/users.py
from fastapi import APIRouter

router = APIRouter(prefix="/users", tags=["users"])

@router.get("/")
async def list_users(): ...

@router.post("/")
async def create_user(): ...

# main.py
from routers import users, items

app.include_router(users.router)
app.include_router(items.router, prefix="/api/v1")

Error Handling

from fastapi import Request
from fastapi.responses import JSONResponse

# Custom exception
class AppError(Exception):
    def __init__(self, message: str, code: int = 400):
        self.message = message
        self.code = code

# Global handler
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    return JSONResponse(
        status_code=exc.code,
        content={"error": exc.message}
    )

# Validation error handler (Pydantic)
from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content={"detail": exc.errors(), "body": exc.body}
    )

Lifespan Events (startup/shutdown)

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup
    await db.connect()
    await redis.connect()
    yield
    # shutdown
    await db.disconnect()
    await redis.disconnect()

app = FastAPI(lifespan=lifespan)

Testing FastAPI

from fastapi.testclient import TestClient

def test_get_user():
    client = TestClient(app)
    response = client.get("/users/1")
    assert response.status_code == 200
    assert response.json()["name"] == "Alice"

# Async test
import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_create_user():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post("/users", json={"name": "Bob", "email": "bob@test.com"})
    assert response.status_code == 201

Interview Talking Points

  • "FastAPI's DI system is its best feature — you declare what a route needs (DB session, current user, feature flags) and FastAPI wires it up. Clean separation, easy to mock in tests."
  • "Pydantic models serve dual purpose: request validation and response serialization. One model definition covers both."
  • "FastAPI is async by default — routes that do I/O should be async def. CPU-bound routes should use run_in_executor."

Related

  • [[Python/Libraries/Pydantic]] — data validation
  • [[Python/Libraries/Asyncio]] — async/await fundamentals
  • [[Python/Language Core/Decorators]] — FastAPI uses decorators heavily
  • [[API Gateway]] — API design principles