Cheat SheetsFastAPIRouting & Structure

Routing & Structure — Cheat Sheet

FastAPI · 3 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Routing & Structure
FastAPI3 topicsQuick revision reference
1

APIRouter — Splitting the App into Modules

APIRouter lets each domain (students, drives, auth) live in its own file with its own prefix, tags, and dependencies — include_router assembles them into one app with one /docs.

  • APIRouter(prefix, tags) per domain file; app.include_router() assembles
  • Prefix/tags/dependencies declared once apply to every route in the router
  • Thin routers (HTTP translation) + fat services (business logic, no HTTP)
  • Version APIs by mounting routers under /api/v1 prefixes
One domain per file; main.py just includes routers
# ── app/routers/students.py ─────────────────
from fastapi import APIRouter, HTTPException

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

@router.get("")                          # → GET /students
def list_students():
    return [{"id": 1, "name": "Asha"}]

@router.get("/{student_id}")             # → GET /students/42
def get_student(student_id: int):
    if student_id != 1:
        raise HTTPException(404, "student not found")
    return {"id": 1, "name": "Asha"}

# ── app/routers/drives.py ───────────────────
from fastapi import APIRouter

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

@router.post("", status_code=201)
def create_drive(payload: dict):
    return payload

# ── app/main.py — assembly only ─────────────
from fastapi import FastAPI
from app.routers import students, drives

app = FastAPI(title="Placement API")
app.include_router(students.router)
app.include_router(drives.router)

# Versioning: mount everything under /api/v1 in one line
# app.include_router(students.router, prefix="/api/v1")
# /docs shows Students and Drives as separate sections automatically
2

Error Handling — Custom Exceptions & Handlers

Raise domain exceptions from services; @app.exception_handler converts them to HTTP responses in one place. Override the validation handler for a consistent error envelope, and never leak stack traces.

  • Domain exceptions in services; @app.exception_handler maps them to status codes
  • Override RequestValidationError to fit 422s into your standard envelope
  • Catch-all Exception handler: log with a reference id, return a bland 500
  • Never leak stack traces, SQL, or file paths in responses
Services raise meaning; one handler maps it to HTTP
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

# ── domain exceptions (services/exceptions.py) — no HTTP here ──
class DomainError(Exception):
    def __init__(self, message: str):
        self.message = message

class StudentNotFound(DomainError): pass
class SeatLimitReached(DomainError): pass
class DriveClosed(DomainError): pass

# ── one translation table (main.py / core/errors.py) ──
STATUS = {StudentNotFound: 404, SeatLimitReached: 409, DriveClosed: 410}

@app.exception_handler(DomainError)
def domain_error_handler(request: Request, exc: DomainError):
    return JSONResponse(
        status_code=STATUS.get(type(exc), 400),
        content={"error": type(exc).__name__, "detail": exc.message},
    )

# ── service code stays pure ──
def register_for_drive(student_id: int, drive_id: int):
    if student_id not in {1, 2}:
        raise StudentNotFound(f"student {student_id} does not exist")
    if drive_id == 7:
        raise SeatLimitReached("drive 7 is full (120/120)")
    return {"registered": True}

@app.post("/drives/{drive_id}/register/{student_id}")
def register(drive_id: int, student_id: int):
    return register_for_drive(student_id, drive_id)

# POST /drives/7/register/1 → 409 {"error":"SeatLimitReached","detail":"drive 7 is full (120/120)"}
# POST /drives/2/register/9 → 404 {"error":"StudentNotFound","detail":"student 9 does not exist"}
3

Settings & Configuration — pydantic-settings and .env

BaseSettings reads typed config from environment variables and .env files — validated at startup, injected via a cached get_settings dependency, with secrets kept out of git entirely.

  • BaseSettings: typed, validated config from env vars + .env (env wins)
  • Required fields without defaults make missing config a startup crash
  • @lru_cache get_settings() + Depends = injectable, test-overridable config
  • .env is gitignored; .env.example documents; prod secrets live in the platform
Types + required fields = misconfiguration caught at boot
# pip install pydantic-settings

# ── app/core/config.py ──────────────────────
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")

    app_name: str = "Placement API"
    env: str = "dev"                       # dev | staging | prod
    debug: bool = False
    database_url: str                      # REQUIRED — no default, crash if absent
    jwt_secret: str                        # REQUIRED
    jwt_ttl_minutes: int = 30
    razorpay_key_id: str | None = None     # optional integration
    allowed_origins: list[str] = ["http://localhost:3000"]

# ── .env (local dev ONLY — in .gitignore) ───
# DATABASE_URL=postgresql://app:app@localhost:5432/placement
# JWT_SECRET=dev-only-not-for-prod
# DEBUG=true
# ALLOWED_ORIGINS=["http://localhost:3000","http://localhost:3001"]

# ── .env.example (committed — the contract, dummy values) ──
# DATABASE_URL=postgresql://user:pass@host:5432/dbname
# JWT_SECRET=change-me

# Missing DATABASE_URL at startup →
#   pydantic ValidationError: database_url: Field required
# The app refuses to boot half-configured. That is a feature.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/fastapi