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 automatically2
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