Module 2 · FastAPI · Deep Dive

FastAPI Advanced

The patterns that turn a one-file demo into a real backend — dependency injection, routers, async done right, CORS, background work, config, and tests. This is the lesson that makes DocChat production-shaped.

BasicIntermediateBuild

Why this matters Your FastAPI core lesson got endpoints working. But the moment DocChat grows a database, a React frontend, an LLM call, and a test suite, a single main.py falls apart. This lesson is the bridge from "it runs" to "it's hireable" — every pattern here is something a UAE interviewer will probe, and something you'll wire into the capstone within the hour.
In this lesson
  1. Dependency Injection with Depends
  2. Dependencies with yield (setup/teardown)
  3. Structuring a bigger app: APIRouter
  4. Async, deeper — when it actually helps
  5. CORS — the frontend gotcha
  6. Middleware & BackgroundTasks
  7. Settings & config from the environment
  8. Testing with TestClient + pytest
  9. Build: refactor DocChat
  10. Check yourself

1 · Dependency Injection with Depends

A dependency is just a function FastAPI runs before your endpoint, passing the result in as an argument. You declare what you need; FastAPI builds it and hands it over. That's the whole idea — and it's the most important pattern in the framework.

from fastapi import Depends, FastAPI

app = FastAPI()

# a dependency is just a function
def pagination(skip: int = 0, limit: int = 20):
    return {"skip": skip, "limit": limit}

@app.get("/documents")
def list_docs(page: dict = Depends(pagination)):
    return page   # {"skip": 0, "limit": 20}

The win is reuse: write the logic once, inject it into ten endpoints. The classic example is a database session. You don't want every route opening and closing a connection by hand — you write one get_db dependency and let FastAPI thread it everywhere.

from sqlalchemy.orm import Session

def get_db():
    db = SessionLocal()        # open a session
    return db

@app.get("/documents/{doc_id}")
def read_doc(doc_id: int, db: Session = Depends(get_db)):
    return db.query(Document).get(doc_id)
PHP bridge: if you ever used a service container (Laravel's app()->make(), Symfony's autowiring), this is the same instinct — declare the contract, let the framework construct it. The difference: in FastAPI it's plain functions and type hints, no config files.
Interview angle: "Why does FastAPI's dependency injection make code more testable?" Because an endpoint never builds its own database or LLM client — it receives them. In a test you call app.dependency_overrides[get_db] = fake_db and swap in a stub with one line. No mocking libraries, no monkey-patching globals. The dependency is the seam.

2 · Dependencies with yield

The get_db above has a bug: it never closes the session. The fix is a dependency with yield. Code before the yield is setup; code after it is teardown, and FastAPI runs it after the response is sent — even if the endpoint raised.

def get_db():
    db = SessionLocal()
    try:
        yield db            # hand the session to the endpoint
    finally:
        db.close()          # always runs — guaranteed cleanup

This is the right shape for anything that needs paired setup/teardown: database sessions, file handles, network clients. The try/finally guarantees the resource is released no matter what happens inside the request.

Mental model A yield dependency is like a context manager (with open(...) as f:) stretched across the whole request. Everything before yield = entering the with; everything after = exiting it.

3 · Structuring a bigger app with APIRouter

One main.py with forty routes is unmaintainable. APIRouter lets you split routes into modules — think of it as a mini-FastAPI you mount onto the main app.

app/routers/documents.py
from fastapi import APIRouter, Depends
from app.db import get_db

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

@router.get("/")
def list_docs(db = Depends(get_db)):
    return db.query(Document).all()

@router.post("/")
def create_doc(title: str, db = Depends(get_db)):
    ...

Then an app factory — a function that builds and returns the app — wires the routers together. A factory keeps construction in one place and makes testing trivial (each test can build a fresh app).

app/main.py
from fastapi import FastAPI
from app.routers import documents, chat

def create_app() -> FastAPI:
    app = FastAPI(title="DocChat")
    app.include_router(documents.router)
    app.include_router(chat.router)
    return app

app = create_app()
PHP bridge: APIRouter ≈ a route group in your old framework (Route::prefix('documents')->group(...)). The prefix and tags are applied to every route inside, so you don't repeat /documents on each one.

4 · Async, deeper

FastAPI is async-native, but async def is not free magic — it helps only for I/O-bound work: waiting on a database, an HTTP call, or an LLM. While one request is await-ing the network, the event loop serves others. That's the entire benefit.

# GOOD: awaiting I/O lets the loop serve other requests meanwhile
@router.post("/ask")
async def ask(question: str):
    answer = await llm_client.complete(question)   # network wait
    return {"answer": answer}
The event-loop trap Inside an async def, never call a blocking function (a slow CPU loop, time.sleep, a sync DB driver). It freezes the entire event loop — every other request stalls. If a library isn't async-aware, either use a plain def endpoint (FastAPI runs it in a threadpool for you) or offload with run_in_threadpool.

Rule of thumb: async def when you'll await something; plain def when the work is synchronous and FastAPI should manage the thread. For DocChat, the LLM and any async DB driver calls are exactly where async pays off.

5 · CORS — the gotcha you will hit

You'll build DocChat's API on localhost:8000 and your Next.js frontend on localhost:3000. The first time React calls the API, the browser blocks it with a CORS error — and nothing is actually wrong with your code.

Why: browsers enforce the same-origin policy. A page served from localhost:3000 is a different origin than an API on localhost:8000 (different port = different origin). The browser refuses to expose the response unless the API explicitly says "I allow that origin" via CORS response headers. Your server must opt in.

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],   # the Next.js dev origin
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
Interview angle: CORS is a browser security feature, not a server one. curl and Postman ignore it entirely — only browsers enforce it. So if your API "works in Postman but not from React," the answer is almost always missing CORS middleware. Saying this cleanly signals real full-stack experience.
PHP bridge: this is the same problem you solved by sending Access-Control-Allow-Origin headers by hand in PHP. CORSMiddleware just sets those headers (and handles the preflight OPTIONS request) for you.

6 · Middleware & BackgroundTasks

Middleware wraps every request: code runs before the endpoint, then after it, on the way out. CORSMiddleware is one example; you can write your own for logging or timing.

@app.middleware("http")
async def add_timing(request, call_next):
    import time
    start = time.perf_counter()
    response = await call_next(request)        # run the endpoint
    response.headers["X-Time-Ms"] = str(time.perf_counter() - start)
    return response

BackgroundTasks let you return a response now and run slow work after. Perfect for DocChat: accept an uploaded document, reply "received" instantly, and process it (chunk + embed) in the background.

from fastapi import BackgroundTasks

def process_document(doc_id: int):
    ...   # chunk, embed, store — the slow part

@router.post("/documents")
def upload(doc_id: int, tasks: BackgroundTasks):
    tasks.add_task(process_document, doc_id)
    return {"status": "processing"}   # returns immediately
When to reach past BackgroundTasks BackgroundTasks runs in the same process — great for quick jobs. For heavy, retryable, or long pipelines you'd graduate to a real queue (Celery, RQ, or a managed worker). For DocChat's scope, background tasks are the right tool.

7 · Settings & config from the environment

Hardcoding an API key or database URL is the fastest way to leak a secret to GitHub. The 2026-standard answer is pydantic-settings: a BaseSettings class that reads values from environment variables (and a local .env file), validated and typed.

pip install pydantic-settings
app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    database_url: str
    openai_api_key: str
    cors_origins: str = "http://localhost:3000"

    model_config = SettingsConfigDict(env_file=".env")

settings = Settings()   # reads env / .env at startup, validates types

Now secrets live in .env (which is .gitignored) and config is one import away. Combine it with DI — a get_settings dependency — and your endpoints stay clean and testable.

Interview angle: "How do you keep secrets out of code?" Environment variables + a typed settings object + a git-ignored .env, with real secrets injected by the host (Render, Railway, Docker) at deploy time. Mentioning that BaseSettings validates the config at startup — so a missing key fails fast instead of at 3am — is the senior touch.

8 · Testing with TestClient + pytest

FastAPI ships a TestClient that calls your app in-process — no running server, no real network. Paired with pytest, you get fast, real HTTP-level tests.

pip install pytest httpx
tests/test_documents.py
from fastapi.testclient import TestClient
from app.main import create_app

client = TestClient(create_app())

def test_list_documents_returns_200():
    response = client.get("/documents/")
    assert response.status_code == 200
    assert isinstance(response.json(), list)

Run it with pytest. Because create_app() is a factory, each test can build a fresh app and override dependencies — e.g. swap get_db for an in-memory database — keeping tests isolated and fast.

PHP bridge: TestClient ≈ Laravel's $this->get('/route')->assertStatus(200). Same idea: drive the app through HTTP without booting a server.

9 · Build it

Your tangible win Refactor DocChat from a single file into a router-based app: move the document endpoints into an APIRouter, add CORSMiddleware for http://localhost:3000 so your Next.js frontend can call it, and write one passing TestClient test. This is the exact structure you'll deploy in the capstone.
app/routers/documents.py
from fastapi import APIRouter, BackgroundTasks, Depends
from app.db import get_db

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

def process_document(doc_id: int):
    ...   # chunk + embed later

@router.get("/")
def list_docs(db = Depends(get_db)):
    return db.query(Document).all()

@router.post("/")
def upload(title: str, tasks: BackgroundTasks, db = Depends(get_db)):
    doc = Document(title=title)
    db.add(doc); db.commit()
    tasks.add_task(process_document, doc.id)
    return {"id": doc.id, "status": "processing"}
app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import documents

def create_app() -> FastAPI:
    app = FastAPI(title="DocChat")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["http://localhost:3000"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    app.include_router(documents.router)
    return app

app = create_app()
tests/test_documents.py
from fastapi.testclient import TestClient
from app.main import create_app

client = TestClient(create_app())

def test_list_documents_returns_200():
    response = client.get("/documents/")
    assert response.status_code == 200

Run uvicorn app.main:app --reload, hit /documents/, then pytest. When the test goes green and the React origin is allowed, DocChat is production-shaped.

10 · Check yourself

Answer from memory — retrieval is what turns "I read it" into "I know it".

Recall quiz

What does a Depends dependency mainly give you?

In a yield dependency, code after yield runs:

Why does the React frontend need CORS enabled?

When does async def actually help performance?

Where should an LLM API key be stored?

Primary source ⭐ FastAPI — Dependencies (official tutorial). The authoritative reference for Depends, yield dependencies, routers, middleware, and testing. Read it alongside this lesson; the framework docs are unusually good.