Back to Home
    FastAPIFastAPI v0.115Intermediate

    FastAPI

    FastAPI routes, request models, dependency injection, authentication, and async patterns.

    10 min read
    fastapipythonapiasync

    Routes & Models

    1 topic

    Basic CRUD

    python
    from fastapi import FastAPI, HTTPException, status
    from pydantic import BaseModel
    from typing import List
    
    app = FastAPI(title="My API", version="1.0")
    
    # Mock DB
    fake_db = []
    id_counter = 1
    
    class UserCreate(BaseModel):
        name: str
        email: str
        age: int = 18
    
    class UserResponse(BaseModel):
        id: int
        name: str
        email: str
    
        class Config:
            from_attributes = True  # use orm_mode=True if Pydantic v1
    
    @app.get("/users", response_model=List[UserResponse])
    async def list_users():
        return fake_db
    
    @app.get("/users/{user_id}", response_model=UserResponse)
    async def get_user(user_id: int):
        user = next((u for u in fake_db if u["id"] == user_id), None)
        if not user:
            raise HTTPException(status_code=404, detail="User not found")
        return user
    
    @app.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
    async def create_user(user: UserCreate):
        global id_counter
        new_user = {
            "id": id_counter,
            "name": user.name,
            "email": user.email,
        }
        fake_db.append(new_user)
        id_counter += 1
        return new_user

    💡 Use response_model to control what fields are returned — never return raw DB objects

    ⚡ status.HTTP_201_CREATED is clearer than the magic number 201

    Introduction: What FastAPI Is, Why It Wins, and When to Use It

    2 topics

    What FastAPI is and why teams pick it

    FastAPI is an async-first Python web framework for building APIs with automatic validation, OpenAPI docs, and high performance through Starlette + Pydantic. Teams choose it because it reduces boilerplate and catches invalid input early.

    python
    from fastapi import FastAPI
    
    app = FastAPI(title="Production API")
    
    @app.get("/health")
    def health_check():
        return {"status": "ok"}

    FastAPI auto-generates Swagger UI at /docs and ReDoc at /redoc.

    Type hints are not decoration here—they drive validation, docs, and editor support.

    For teams migrating from Flask, this usually means less manual request parsing and validation code.

    overviewhealthcheck

    FastAPI vs Flask/Django in practical terms

    Use FastAPI when API-first development, async I/O, and strict contracts matter. Flask remains simpler for tiny services. Django is stronger for monoliths with admin, ORM, and batteries-included workflows.

    python
    # Rule-of-thumb matrix (pseudo-code as comments)
    # If you need:
    # - API-first + async external calls + OpenAPI docs -> FastAPI
    # - Very small sync service or prototype -> Flask
    # - Full web app + admin + ORM + templates -> Django
    
    USE_FASTAPI_WHEN = [
        "many API endpoints",
        "strict input/output schemas",
        "high throughput with async I/O",
        "multiple client teams relying on API docs",
    ]
    
    AVOID_FASTAPI_WHEN = [
        "you need Django admin-like productivity out of the box",
        "team lacks async basics and cannot invest now",
        "your app is mostly server-rendered pages",
    ]

    FastAPI is not magic speed for CPU-heavy workloads; optimize logic and infrastructure too.

    Adoption success depends on consistent schema design and code organization.

    tradeoffsarchitecture-decision

    Dependency Injection

    1 topic

    Depends

    python
    from fastapi import Depends, HTTPException
    from fastapi.security import OAuth2PasswordBearer
    
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    
    async def get_current_user(token: str = Depends(oauth2_scheme)):
        user = await verify_token(token)
        if not user:
            raise HTTPException(status_code=401, detail="Invalid token")
        return user
    
    async def require_admin(current_user = Depends(get_current_user)):
        if not current_user.is_admin:
            raise HTTPException(status_code=403, detail="Forbidden")
        return current_user
    
    # Use in routes
    @app.delete("/users/{user_id}")
    async def delete_user(
        user_id: int,
        _: User = Depends(require_admin),  # Auth enforced
    ):
        await db.delete_user(user_id)
        return {"message": "Deleted"}

    💡 Dependencies compose cleanly — require_admin calls get_current_user automatically

    ⚡ Reuse dependencies with Depends() to avoid copy-pasting auth checks

    Setup, Installation, and Your First Working API

    3 topics

    Step-by-step project setup

    Create an isolated environment, install runtime dependencies, and run with Uvicorn. This is the baseline for every service.

    bash
    python -m venv .venv
    source .venv/bin/activate  # Windows: .venv\Scripts\activate
    
    pip install --upgrade pip
    pip install fastapi uvicorn[standard] pydantic-settings sqlalchemy alembic python-jose[cryptography] passlib[bcrypt]
    
    # optional dev tools
    pip install pytest httpx ruff mypy

    Pin dependencies in requirements.txt or use a lockfile (poetry/pdm/pip-tools).

    Use pydantic-settings for typed environment config.

    Install uvicorn[standard] for faster loop and production-friendly extras.

    installationvenvdependencies

    Minimal Hello World + request/response flow

    A minimal app demonstrates endpoint declaration and response serialization. Flow: client request -> router -> function -> JSON response.

    python
    from fastapi import FastAPI
    
    app = FastAPI()
    
    @app.get("/")
    async def root():
        return {"message": "Hello, FastAPI"}
    
    # Run:
    # uvicorn app.main:app --reload

    Use async def for I/O-heavy handlers (DB/network/file).

    Use --reload only in development.

    Test quickly with: curl http://127.0.0.1:8000/

    hello-worldrequest-flow

    Simple architecture diagram (text-based)

    A practical vertical flow for production services including middleware, auth, service layer, and DB.

    Data Layer

    App Layer

    Client App

    Nginx

    Uvicorn Workers

    FastAPI Middleware

    Router Layer

    Dependency Injection

    Service Layer

    ORM (SQLAlchemy)

    PostgreSQL

    Redis Cache

    Structured Logging

    Keep business rules in services, not in route handlers.

    Dependencies are ideal for auth, DB sessions, and tenant context.

    Nginx handles TLS termination and buffering in front of Uvicorn.

    architecturedeployment

    Core Features and Building a Real CRUD API

    2 topics

    Path params, query params, and request/response schemas

    Use Pydantic models to validate input and shape output. This enforces API contracts and prevents accidental data leaks.

    python
    from fastapi import FastAPI, Query
    from pydantic import BaseModel, Field
    
    app = FastAPI()
    
    class TodoCreate(BaseModel):
        title: str = Field(min_length=3, max_length=120)
        done: bool = False
    
    class TodoOut(BaseModel):
        id: int
        title: str
        done: bool
    
    TODOS = [{"id": 1, "title": "Ship API", "done": False}]
    
    @app.get("/todos/{todo_id}", response_model=TodoOut)
    def get_todo(todo_id: int):
        return next(t for t in TODOS if t["id"] == todo_id)
    
    @app.get("/todos", response_model=list[TodoOut])
    def list_todos(limit: int = Query(default=10, ge=1, le=100)):
        return TODOS[:limit]
    
    @app.post("/todos", response_model=TodoOut, status_code=201)
    def create_todo(payload: TodoCreate):
        todo = {"id": len(TODOS) + 1, **payload.model_dump()}
        TODOS.append(todo)
        return todo

    response_model strips unexpected fields and documents response shape clearly.

    Use Field constraints instead of manual if-statements whenever possible.

    Validation errors return 422 with detailed location metadata.

    pydanticvalidationresponse-model

    Production folder structure and separation of concerns

    Organize by responsibility to keep handlers thin and testable: routes, schemas, services, repositories, core config.

    python
    app/
      main.py
      api/
        v1/
          routes_todos.py
      schemas/
        todo.py
      services/
        todo_service.py
      repositories/
        todo_repo.py
      db/
        session.py
        models.py
      core/
        config.py
        security.py
    
    # app/api/v1/routes_todos.py
    from fastapi import APIRouter, Depends
    from app.schemas.todo import TodoCreate, TodoOut
    from app.services.todo_service import TodoService
    
    router = APIRouter(prefix="/todos", tags=["todos"])
    
    @router.post("", response_model=TodoOut)
    def create_todo(payload: TodoCreate, service: TodoService = Depends()):
        return service.create(payload)
    
    # app/services/todo_service.py
    class TodoService:
        def create(self, payload):
            # business rules go here (dedupe, audit, etc.)
            return {"id": 123, **payload.model_dump()}

    Route handlers should orchestrate, not implement heavy business logic.

    Service layer simplifies unit testing and future refactors.

    Add API versioning early (/api/v1) to avoid painful breaking changes.

    project-structurecrudservice-layer

    Advanced Concepts: DI, Middleware, JWT, Async/Sync, and Database CRUD

    2 topics

    Dependency Injection, middleware, and JWT auth

    Use dependencies for cross-cutting concerns and middleware for request lifecycle operations. JWT is a common stateless auth strategy.

    python
    from datetime import datetime, timedelta, timezone
    from fastapi import FastAPI, Depends, HTTPException, Request
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    from jose import jwt, JWTError
    
    SECRET = "change-me"
    ALGO = "HS256"
    security = HTTPBearer()
    app = FastAPI()
    
    @app.middleware("http")
    async def add_timing_header(request: Request, call_next):
        response = await call_next(request)
        response.headers["X-Service"] = "fastapi-todo"
        return response
    
    def create_access_token(sub: str, minutes: int = 30):
        exp = datetime.now(timezone.utc) + timedelta(minutes=minutes)
        return jwt.encode({"sub": sub, "exp": exp}, SECRET, algorithm=ALGO)
    
    def get_current_user(creds: HTTPAuthorizationCredentials = Depends(security)):
        token = creds.credentials
        try:
            payload = jwt.decode(token, SECRET, algorithms=[ALGO])
            return payload["sub"]
        except JWTError:
            raise HTTPException(status_code=401, detail="Invalid token")
    
    @app.get("/me")
    def me(user_id: str = Depends(get_current_user)):
        return {"user_id": user_id}

    Rotate JWT secrets and keep them in environment variables, not source code.

    Prefer short-lived access tokens plus refresh token strategy for real apps.

    Middleware is good for correlation IDs, metrics, and security headers.

    dependency-injectionmiddlewarejwt

    SQLAlchemy integration with one model and CRUD

    Keep DB session lifecycle controlled with a dependency. Map ORM model + Pydantic schemas + service-level operations.

    python
    from sqlalchemy import create_engine, String, Boolean
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker, Session
    from fastapi import FastAPI, Depends, HTTPException
    from pydantic import BaseModel
    
    DATABASE_URL = "sqlite:///./app.db"
    engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
    SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    
    class Base(DeclarativeBase):
        pass
    
    class Todo(Base):
        __tablename__ = "todos"
        id: Mapped[int] = mapped_column(primary_key=True, index=True)
        title: Mapped[str] = mapped_column(String(120), index=True)
        done: Mapped[bool] = mapped_column(Boolean, default=False)
    
    Base.metadata.create_all(bind=engine)
    
    class TodoCreate(BaseModel):
        title: str
    
    class TodoOut(BaseModel):
        id: int
        title: str
        done: bool
    
    app = FastAPI()
    
    def get_db():
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()
    
    @app.post("/db/todos", response_model=TodoOut)
    def create_todo(payload: TodoCreate, db: Session = Depends(get_db)):
        todo = Todo(title=payload.title)
        db.add(todo)
        db.commit()
        db.refresh(todo)
        return todo
    
    @app.get("/db/todos/{todo_id}", response_model=TodoOut)
    def get_todo(todo_id: int, db: Session = Depends(get_db)):
        todo = db.get(Todo, todo_id)
        if not todo:
            raise HTTPException(404, "Todo not found")
        return todo

    Use Alembic migrations instead of create_all in production.

    For PostgreSQL, switch DATABASE_URL and remove SQLite-specific args.

    Keep transactions small and explicit to reduce lock contention.

    sqlalchemydatabasecrud

    Production Best Practices, Performance, and Common Mistakes

    2 topics

    Error handling, logging, env config, and Docker/Nginx deployment

    Production APIs need predictable errors, structured logs, and containerized runtime. Pair Uvicorn workers behind Nginx for stable edge behavior.

    python
    from fastapi import FastAPI, Request
    from fastapi.responses import JSONResponse
    from pydantic_settings import BaseSettings
    import logging
    
    class Settings(BaseSettings):
        app_name: str = "todo-api"
        debug: bool = False
        jwt_secret: str
        class Config:
            env_file = ".env"
    
    settings = Settings()
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
    logger = logging.getLogger("api")
    
    app = FastAPI(title=settings.app_name)
    
    @app.exception_handler(ValueError)
    async def value_error_handler(_: Request, exc: ValueError):
        return JSONResponse(status_code=400, content={"error": str(exc)})
    
    @app.get("/boom")
    def boom():
        logger.info("Boom endpoint called")
        raise ValueError("Invalid business input")
    
    # Dockerfile (minimal)
    # FROM python:3.12-slim
    # WORKDIR /app
    # COPY requirements.txt .
    # RUN pip install -r requirements.txt
    # COPY . .
    # CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

    Use JSON logs in production and include request_id/user_id for traceability.

    Never hardcode secrets; use environment variables or secret managers.

    Nginx handles TLS and can protect app workers from slow clients.

    error-handlingloggingdockernginx

    Performance, scaling, and mistakes to avoid

    FastAPI performs well with async I/O and efficient serialization, but blocking calls inside async routes and poor schema design can erase gains.

    python
    from fastapi import FastAPI
    import httpx
    from fastapi.concurrency import run_in_threadpool
    
    app = FastAPI()
    
    # Good async I/O
    @app.get("/external")
    async def call_external():
        async with httpx.AsyncClient(timeout=5.0) as client:
            r = await client.get("https://httpbin.org/get")
        return {"status": r.status_code}
    
    # If you must call blocking code, move it off event loop
    
    def blocking_cpu_or_legacy_call():
        return sum(range(10_000_000))
    
    @app.get("/blocking-safe")
    async def blocking_safe():
        result = await run_in_threadpool(blocking_cpu_or_legacy_call)
        return {"result": result}
    
    # Example curl:
    # curl -H "Authorization: Bearer <token>" http://localhost:8000/me

    Do not call synchronous DB/network clients directly inside async handlers.

    Add caching (Redis) for hot read endpoints to reduce DB load.

    Apply rate limiting at gateway or app layer for abusive traffic patterns.

    Avoid mutable global state for request-specific data; use dependencies/context.

    performanceasyncscalingpitfalls

    Related Articles

    Background reading and deeper explanations for this sheet.

    Agent Routing vs Model Routing: Real Production Patterns, Tradeoffs, and Implementation Guide

    Choosing between agent routing and model routing can make or break your AI product’s cost, speed, and reliability. In this practical guide, you’ll learn what each pattern is, when to use it, and how production teams combine both to ship robust systems. We’ll walk through real examples, architecture patterns, and code you can adapt immediately.

    keyword overlap

    AI Product Pricing Strategies: How to Monetize for Growth, Margin, and Customer Trust

    Pricing an AI product is not just a finance decision—it shapes adoption, retention, margins, and user trust. In this practical guide, you’ll learn how to choose the right pricing model, align price with value and costs, and avoid common mistakes using real-world examples and implementation frameworks.

    keyword overlap

    Build an Agentic App with FastAPI and Azure AI Foundry: A Practical Beginner-to-Pro Guide

    Learn how to design, build, and deploy an agentic application using FastAPI and Azure AI Foundry with practical, real-world examples. This guide walks you from architecture and setup to tool-calling, memory, observability, and production deployment patterns.

    keyword overlap

    Building Autonomous Agents Without Overengineering: A Practical Guide for Real-World Teams

    Autonomous agents can automate meaningful work, but many projects fail because teams overbuild too early. This practical guide shows you how to design, ship, and improve useful agents with simple patterns, clear guardrails, and real-world examples—without turning your stack into a science project.

    keyword overlap